##// END OF EJS Templates
comments: multiple changes on comments navigation/display logic...
milka -
r4543:624997f0 default
parent child Browse files
Show More

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

@@ -1,486 +1,486 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import datetime
24 24
25 25 from pyramid.view import view_config
26 26 from pyramid.renderers import render_to_response
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib.celerylib import run_task, tasks
29 29 from rhodecode.lib.utils2 import AttributeDict
30 30 from rhodecode.model.db import User
31 31 from rhodecode.model.notification import EmailNotificationModel
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class DebugStyleView(BaseAppView):
37 37
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40
41 41 return c
42 42
43 43 @view_config(
44 44 route_name='debug_style_home', request_method='GET',
45 45 renderer=None)
46 46 def index(self):
47 47 c = self.load_default_context()
48 48 c.active = 'index'
49 49
50 50 return render_to_response(
51 51 'debug_style/index.html', self._get_template_context(c),
52 52 request=self.request)
53 53
54 54 @view_config(
55 55 route_name='debug_style_email', request_method='GET',
56 56 renderer=None)
57 57 @view_config(
58 58 route_name='debug_style_email_plain_rendered', request_method='GET',
59 59 renderer=None)
60 60 def render_email(self):
61 61 c = self.load_default_context()
62 62 email_id = self.request.matchdict['email_id']
63 63 c.active = 'emails'
64 64
65 65 pr = AttributeDict(
66 66 pull_request_id=123,
67 67 title='digital_ocean: fix redis, elastic search start on boot, '
68 68 'fix fd limits on supervisor, set postgres 11 version',
69 69 description='''
70 70 Check if we should use full-topic or mini-topic.
71 71
72 72 - full topic produces some problems with merge states etc
73 73 - server-mini-topic needs probably tweeks.
74 74 ''',
75 75 repo_name='foobar',
76 76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
77 77 target_ref_parts=AttributeDict(type='branch', name='master'),
78 78 )
79 79
80 80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
81 81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
82 82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
83 83 # file/commit changes for PR update
84 84 commit_changes = AttributeDict({
85 85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
86 86 'removed': ['eeeeeeeeeee'],
87 87 })
88 88
89 89 file_changes = AttributeDict({
90 90 'added': ['a/file1.md', 'file2.py'],
91 91 'modified': ['b/modified_file.rst'],
92 92 'removed': ['.idea'],
93 93 })
94 94
95 95 exc_traceback = {
96 96 'exc_utc_date': '2020-03-26T12:54:50.683281',
97 97 'exc_id': 139638856342656,
98 98 'exc_timestamp': '1585227290.683288',
99 99 'version': 'v1',
100 100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
101 101 'exc_type': 'AttributeError'
102 102 }
103 103
104 104 email_kwargs = {
105 105 'test': {},
106 106
107 107 'message': {
108 108 'body': 'message body !'
109 109 },
110 110
111 111 'email_test': {
112 112 'user': user,
113 113 'date': datetime.datetime.now(),
114 114 },
115 115
116 116 'exception': {
117 117 'email_prefix': '[RHODECODE ERROR]',
118 118 'exc_id': exc_traceback['exc_id'],
119 119 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
120 120 'exc_type_name': 'NameError',
121 121 'exc_traceback': exc_traceback,
122 122 },
123 123
124 124 'password_reset': {
125 125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
126 126
127 127 'user': user,
128 128 'date': datetime.datetime.now(),
129 129 'email': 'test@rhodecode.com',
130 130 'first_admin_email': User.get_first_super_admin().email
131 131 },
132 132
133 133 'password_reset_confirmation': {
134 134 'new_password': 'new-password-example',
135 135 'user': user,
136 136 'date': datetime.datetime.now(),
137 137 'email': 'test@rhodecode.com',
138 138 'first_admin_email': User.get_first_super_admin().email
139 139 },
140 140
141 141 'registration': {
142 142 'user': user,
143 143 'date': datetime.datetime.now(),
144 144 },
145 145
146 146 'pull_request_comment': {
147 147 'user': user,
148 148
149 149 'status_change': None,
150 150 'status_change_type': None,
151 151
152 152 'pull_request': pr,
153 153 'pull_request_commits': [],
154 154
155 155 'pull_request_target_repo': target_repo,
156 156 'pull_request_target_repo_url': 'http://target-repo/url',
157 157
158 158 'pull_request_source_repo': source_repo,
159 159 'pull_request_source_repo_url': 'http://source-repo/url',
160 160
161 161 'pull_request_url': 'http://localhost/pr1',
162 162 'pr_comment_url': 'http://comment-url',
163 163 'pr_comment_reply_url': 'http://comment-url#reply',
164 164
165 165 'comment_file': None,
166 166 'comment_line': None,
167 167 'comment_type': 'note',
168 168 'comment_body': 'This is my comment body. *I like !*',
169 169 'comment_id': 2048,
170 170 'renderer_type': 'markdown',
171 171 'mention': True,
172 172
173 173 },
174 174
175 175 'pull_request_comment+status': {
176 176 'user': user,
177 177
178 178 'status_change': 'approved',
179 179 'status_change_type': 'approved',
180 180
181 181 'pull_request': pr,
182 182 'pull_request_commits': [],
183 183
184 184 'pull_request_target_repo': target_repo,
185 185 'pull_request_target_repo_url': 'http://target-repo/url',
186 186
187 187 'pull_request_source_repo': source_repo,
188 188 'pull_request_source_repo_url': 'http://source-repo/url',
189 189
190 190 'pull_request_url': 'http://localhost/pr1',
191 191 'pr_comment_url': 'http://comment-url',
192 192 'pr_comment_reply_url': 'http://comment-url#reply',
193 193
194 194 'comment_type': 'todo',
195 195 'comment_file': None,
196 196 'comment_line': None,
197 197 'comment_body': '''
198 198 I think something like this would be better
199 199
200 200 ```py
201 201 // markdown renderer
202 202
203 203 def db():
204 204 global connection
205 205 return connection
206 206
207 207 ```
208 208
209 209 ''',
210 210 'comment_id': 2048,
211 211 'renderer_type': 'markdown',
212 212 'mention': True,
213 213
214 214 },
215 215
216 216 'pull_request_comment+file': {
217 217 'user': user,
218 218
219 219 'status_change': None,
220 220 'status_change_type': None,
221 221
222 222 'pull_request': pr,
223 223 'pull_request_commits': [],
224 224
225 225 'pull_request_target_repo': target_repo,
226 226 'pull_request_target_repo_url': 'http://target-repo/url',
227 227
228 228 'pull_request_source_repo': source_repo,
229 229 'pull_request_source_repo_url': 'http://source-repo/url',
230 230
231 231 'pull_request_url': 'http://localhost/pr1',
232 232
233 233 'pr_comment_url': 'http://comment-url',
234 234 'pr_comment_reply_url': 'http://comment-url#reply',
235 235
236 236 'comment_file': 'rhodecode/model/get_flow_commits',
237 237 'comment_line': 'o1210',
238 238 'comment_type': 'todo',
239 239 'comment_body': '''
240 240 I like this !
241 241
242 242 But please check this code
243 243
244 244 .. code-block:: javascript
245 245
246 246 // THIS IS RST CODE
247 247
248 248 this.createResolutionComment = function(commentId) {
249 249 // hide the trigger text
250 250 $('#resolve-comment-{0}'.format(commentId)).hide();
251 251
252 252 var comment = $('#comment-'+commentId);
253 253 var commentData = comment.data();
254 254 if (commentData.commentInline) {
255 this.createComment(comment, commentId)
255 this.createComment(comment, f_path, line_no, commentId)
256 256 } else {
257 257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
258 258 }
259 259
260 260 return false;
261 261 };
262 262
263 263 This should work better !
264 264 ''',
265 265 'comment_id': 2048,
266 266 'renderer_type': 'rst',
267 267 'mention': True,
268 268
269 269 },
270 270
271 271 'pull_request_update': {
272 272 'updating_user': user,
273 273
274 274 'status_change': None,
275 275 'status_change_type': None,
276 276
277 277 'pull_request': pr,
278 278 'pull_request_commits': [],
279 279
280 280 'pull_request_target_repo': target_repo,
281 281 'pull_request_target_repo_url': 'http://target-repo/url',
282 282
283 283 'pull_request_source_repo': source_repo,
284 284 'pull_request_source_repo_url': 'http://source-repo/url',
285 285
286 286 'pull_request_url': 'http://localhost/pr1',
287 287
288 288 # update comment links
289 289 'pr_comment_url': 'http://comment-url',
290 290 'pr_comment_reply_url': 'http://comment-url#reply',
291 291 'ancestor_commit_id': 'f39bd443',
292 292 'added_commits': commit_changes.added,
293 293 'removed_commits': commit_changes.removed,
294 294 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
295 295 'added_files': file_changes.added,
296 296 'modified_files': file_changes.modified,
297 297 'removed_files': file_changes.removed,
298 298 },
299 299
300 300 'cs_comment': {
301 301 'user': user,
302 302 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
303 303 'status_change': None,
304 304 'status_change_type': None,
305 305
306 306 'commit_target_repo_url': 'http://foo.example.com/#comment1',
307 307 'repo_name': 'test-repo',
308 308 'comment_type': 'note',
309 309 'comment_file': None,
310 310 'comment_line': None,
311 311 'commit_comment_url': 'http://comment-url',
312 312 'commit_comment_reply_url': 'http://comment-url#reply',
313 313 'comment_body': 'This is my comment body. *I like !*',
314 314 'comment_id': 2048,
315 315 'renderer_type': 'markdown',
316 316 'mention': True,
317 317 },
318 318
319 319 'cs_comment+status': {
320 320 'user': user,
321 321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
322 322 'status_change': 'approved',
323 323 'status_change_type': 'approved',
324 324
325 325 'commit_target_repo_url': 'http://foo.example.com/#comment1',
326 326 'repo_name': 'test-repo',
327 327 'comment_type': 'note',
328 328 'comment_file': None,
329 329 'comment_line': None,
330 330 'commit_comment_url': 'http://comment-url',
331 331 'commit_comment_reply_url': 'http://comment-url#reply',
332 332 'comment_body': '''
333 333 Hello **world**
334 334
335 335 This is a multiline comment :)
336 336
337 337 - list
338 338 - list2
339 339 ''',
340 340 'comment_id': 2048,
341 341 'renderer_type': 'markdown',
342 342 'mention': True,
343 343 },
344 344
345 345 'cs_comment+file': {
346 346 'user': user,
347 347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
348 348 'status_change': None,
349 349 'status_change_type': None,
350 350
351 351 'commit_target_repo_url': 'http://foo.example.com/#comment1',
352 352 'repo_name': 'test-repo',
353 353
354 354 'comment_type': 'note',
355 355 'comment_file': 'test-file.py',
356 356 'comment_line': 'n100',
357 357
358 358 'commit_comment_url': 'http://comment-url',
359 359 'commit_comment_reply_url': 'http://comment-url#reply',
360 360 'comment_body': 'This is my comment body. *I like !*',
361 361 'comment_id': 2048,
362 362 'renderer_type': 'markdown',
363 363 'mention': True,
364 364 },
365 365
366 366 'pull_request': {
367 367 'user': user,
368 368 'pull_request': pr,
369 369 'pull_request_commits': [
370 370 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
371 371 my-account: moved email closer to profile as it's similar data just moved outside.
372 372 '''),
373 373 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
374 374 users: description edit fixes
375 375
376 376 - tests
377 377 - added metatags info
378 378 '''),
379 379 ],
380 380
381 381 'pull_request_target_repo': target_repo,
382 382 'pull_request_target_repo_url': 'http://target-repo/url',
383 383
384 384 'pull_request_source_repo': source_repo,
385 385 'pull_request_source_repo_url': 'http://source-repo/url',
386 386
387 387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 388 'user_role': 'reviewer',
389 389 },
390 390
391 391 'pull_request+reviewer_role': {
392 392 'user': user,
393 393 'pull_request': pr,
394 394 'pull_request_commits': [
395 395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
396 396 my-account: moved email closer to profile as it's similar data just moved outside.
397 397 '''),
398 398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
399 399 users: description edit fixes
400 400
401 401 - tests
402 402 - added metatags info
403 403 '''),
404 404 ],
405 405
406 406 'pull_request_target_repo': target_repo,
407 407 'pull_request_target_repo_url': 'http://target-repo/url',
408 408
409 409 'pull_request_source_repo': source_repo,
410 410 'pull_request_source_repo_url': 'http://source-repo/url',
411 411
412 412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 413 'user_role': 'reviewer',
414 414 },
415 415
416 416 'pull_request+observer_role': {
417 417 'user': user,
418 418 'pull_request': pr,
419 419 'pull_request_commits': [
420 420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 421 my-account: moved email closer to profile as it's similar data just moved outside.
422 422 '''),
423 423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 424 users: description edit fixes
425 425
426 426 - tests
427 427 - added metatags info
428 428 '''),
429 429 ],
430 430
431 431 'pull_request_target_repo': target_repo,
432 432 'pull_request_target_repo_url': 'http://target-repo/url',
433 433
434 434 'pull_request_source_repo': source_repo,
435 435 'pull_request_source_repo_url': 'http://source-repo/url',
436 436
437 437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 438 'user_role': 'observer'
439 439 }
440 440 }
441 441
442 442 template_type = email_id.split('+')[0]
443 443 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
444 444 template_type, **email_kwargs.get(email_id, {}))
445 445
446 446 test_email = self.request.GET.get('email')
447 447 if test_email:
448 448 recipients = [test_email]
449 449 run_task(tasks.send_email, recipients, c.subject,
450 450 c.email_body_plaintext, c.email_body)
451 451
452 452 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
453 453 template = 'debug_style/email_plain_rendered.mako'
454 454 else:
455 455 template = 'debug_style/email.mako'
456 456 return render_to_response(
457 457 template, self._get_template_context(c),
458 458 request=self.request)
459 459
460 460 @view_config(
461 461 route_name='debug_style_template', request_method='GET',
462 462 renderer=None)
463 463 def template(self):
464 464 t_path = self.request.matchdict['t_path']
465 465 c = self.load_default_context()
466 466 c.active = os.path.splitext(t_path)[0]
467 467 c.came_from = ''
468 468 # NOTE(marcink): extend the email types with variations based on data sets
469 469 c.email_types = {
470 470 'cs_comment+file': {},
471 471 'cs_comment+status': {},
472 472
473 473 'pull_request_comment+file': {},
474 474 'pull_request_comment+status': {},
475 475
476 476 'pull_request_update': {},
477 477
478 478 'pull_request+reviewer_role': {},
479 479 'pull_request+observer_role': {},
480 480 }
481 481 c.email_types.update(EmailNotificationModel.email_types)
482 482
483 483 return render_to_response(
484 484 'debug_style/' + t_path, self._get_template_context(c),
485 485 request=self.request)
486 486
@@ -1,791 +1,795 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 from pyramid.httpexceptions import (
25 25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 31 from rhodecode.apps.file_store import utils as store_utils
32 32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33 33
34 34 from rhodecode.lib import diffs, codeblocks, channelstream
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.compat import OrderedDict
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
48 48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 49 ChangesetCommentHistory
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 def _update_with_GET(params, request):
59 59 for k in ['diff1', 'diff2', 'diff']:
60 60 params[k] += request.GET.getall(k)
61 61
62 62
63 63 class RepoCommitsView(RepoAppView):
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 66 c.rhodecode_repo = self.rhodecode_vcs_repo
67 67
68 68 return c
69 69
70 70 def _is_diff_cache_enabled(self, target_repo):
71 71 caching_enabled = self._get_general_setting(
72 72 target_repo, 'rhodecode_diff_cache')
73 73 log.debug('Diff caching enabled: %s', caching_enabled)
74 74 return caching_enabled
75 75
76 76 def _commit(self, commit_id_range, method):
77 77 _ = self.request.translate
78 78 c = self.load_default_context()
79 79 c.fulldiff = self.request.GET.get('fulldiff')
80 80
81 81 # fetch global flags of ignore ws or context lines
82 82 diff_context = get_diff_context(self.request)
83 83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84 84
85 85 # diff_limit will cut off the whole diff if the limit is applied
86 86 # otherwise it will just hide the big files from the front-end
87 87 diff_limit = c.visual.cut_off_limit_diff
88 88 file_limit = c.visual.cut_off_limit_file
89 89
90 90 # get ranges of commit ids if preset
91 91 commit_range = commit_id_range.split('...')[:2]
92 92
93 93 try:
94 94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 95 'message', 'parents']
96 96 if self.rhodecode_vcs_repo.alias == 'hg':
97 97 pre_load += ['hidden', 'obsolete', 'phase']
98 98
99 99 if len(commit_range) == 2:
100 100 commits = self.rhodecode_vcs_repo.get_commits(
101 101 start_id=commit_range[0], end_id=commit_range[1],
102 102 pre_load=pre_load, translate_tags=False)
103 103 commits = list(commits)
104 104 else:
105 105 commits = [self.rhodecode_vcs_repo.get_commit(
106 106 commit_id=commit_id_range, pre_load=pre_load)]
107 107
108 108 c.commit_ranges = commits
109 109 if not c.commit_ranges:
110 110 raise RepositoryError('The commit range returned an empty result')
111 111 except CommitDoesNotExistError as e:
112 112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 113 h.flash(msg, category='error')
114 114 raise HTTPNotFound()
115 115 except Exception:
116 116 log.exception("General failure")
117 117 raise HTTPNotFound()
118 118 single_commit = len(c.commit_ranges) == 1
119 119
120 120 c.changes = OrderedDict()
121 121 c.lines_added = 0
122 122 c.lines_deleted = 0
123 123
124 124 # auto collapse if we have more than limit
125 125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 127
128 128 c.commit_statuses = ChangesetStatus.STATUSES
129 129 c.inline_comments = []
130 130 c.files = []
131 131
132 132 c.comments = []
133 133 c.unresolved_comments = []
134 134 c.resolved_comments = []
135 135
136 136 # Single commit
137 137 if single_commit:
138 138 commit = c.commit_ranges[0]
139 139 c.comments = CommentsModel().get_comments(
140 140 self.db_repo.repo_id,
141 141 revision=commit.raw_id)
142 142
143 143 # comments from PR
144 144 statuses = ChangesetStatusModel().get_statuses(
145 145 self.db_repo.repo_id, commit.raw_id,
146 146 with_revisions=True)
147 147
148 148 prs = set()
149 149 reviewers = list()
150 150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 151 for c_status in statuses:
152 152
153 153 # extract associated pull-requests from votes
154 154 if c_status.pull_request:
155 155 prs.add(c_status.pull_request)
156 156
157 157 # extract reviewers
158 158 _user_id = c_status.author.user_id
159 159 if _user_id not in reviewers_duplicates:
160 160 reviewers.append(
161 161 StrictAttributeDict({
162 162 'user': c_status.author,
163 163
164 164 # fake attributed for commit, page that we don't have
165 165 # but we share the display with PR page
166 166 'mandatory': False,
167 167 'reasons': [],
168 168 'rule_user_group_data': lambda: None
169 169 })
170 170 )
171 171 reviewers_duplicates.add(_user_id)
172 172
173 173 c.reviewers_count = len(reviewers)
174 174 c.observers_count = 0
175 175
176 176 # from associated statuses, check the pull requests, and
177 177 # show comments from them
178 178 for pr in prs:
179 179 c.comments.extend(pr.comments)
180 180
181 181 c.unresolved_comments = CommentsModel()\
182 182 .get_commit_unresolved_todos(commit.raw_id)
183 183 c.resolved_comments = CommentsModel()\
184 184 .get_commit_resolved_todos(commit.raw_id)
185 185
186 186 c.inline_comments_flat = CommentsModel()\
187 187 .get_commit_inline_comments(commit.raw_id)
188 188
189 189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
190 190 statuses, reviewers)
191 191
192 192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
193 193
194 194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
195 195
196 196 for review_obj, member, reasons, mandatory, status in review_statuses:
197 197 member_reviewer = h.reviewer_as_json(
198 198 member, reasons=reasons, mandatory=mandatory, role=None,
199 199 user_group=None
200 200 )
201 201
202 202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
203 203 member_reviewer['review_status'] = current_review_status
204 204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
205 205 member_reviewer['allowed_to_update'] = False
206 206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
207 207
208 208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
209 209
210 210 # NOTE(marcink): this uses the same voting logic as in pull-requests
211 211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
212 212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
213 213
214 214 diff = None
215 215 # Iterate over ranges (default commit view is always one commit)
216 216 for commit in c.commit_ranges:
217 217 c.changes[commit.raw_id] = []
218 218
219 219 commit2 = commit
220 220 commit1 = commit.first_parent
221 221
222 222 if method == 'show':
223 223 inline_comments = CommentsModel().get_inline_comments(
224 224 self.db_repo.repo_id, revision=commit.raw_id)
225 225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 226 inline_comments))
227 227 c.inline_comments = inline_comments
228 228
229 229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 230 self.db_repo)
231 231 cache_file_path = diff_cache_exist(
232 232 cache_path, 'diff', commit.raw_id,
233 233 hide_whitespace_changes, diff_context, c.fulldiff)
234 234
235 235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 236 force_recache = str2bool(self.request.GET.get('force_recache'))
237 237
238 238 cached_diff = None
239 239 if caching_enabled:
240 240 cached_diff = load_cached_diff(cache_file_path)
241 241
242 242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 243 if not force_recache and has_proper_diff_cache:
244 244 diffset = cached_diff['diff']
245 245 else:
246 246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 247 commit1, commit2,
248 248 ignore_whitespace=hide_whitespace_changes,
249 249 context=diff_context)
250 250
251 251 diff_processor = diffs.DiffProcessor(
252 252 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 253 file_limit=file_limit, show_full_diff=c.fulldiff)
254 254
255 255 _parsed = diff_processor.prepare()
256 256
257 257 diffset = codeblocks.DiffSet(
258 258 repo_name=self.db_repo_name,
259 259 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 260 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 261
262 262 diffset = self.path_filter.render_patchset_filtered(
263 263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 264
265 265 # save cached diff
266 266 if caching_enabled:
267 267 cache_diff(cache_file_path, diffset, None)
268 268
269 269 c.limited_diff = diffset.limited_diff
270 270 c.changes[commit.raw_id] = diffset
271 271 else:
272 272 # TODO(marcink): no cache usage here...
273 273 _diff = self.rhodecode_vcs_repo.get_diff(
274 274 commit1, commit2,
275 275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 276 diff_processor = diffs.DiffProcessor(
277 277 _diff, format='newdiff', diff_limit=diff_limit,
278 278 file_limit=file_limit, show_full_diff=c.fulldiff)
279 279 # downloads/raw we only need RAW diff nothing else
280 280 diff = self.path_filter.get_raw_patch(diff_processor)
281 281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282 282
283 283 # sort comments by how they were generated
284 284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 285 c.at_version_num = None
286 286
287 287 if len(c.commit_ranges) == 1:
288 288 c.commit = c.commit_ranges[0]
289 289 c.parent_tmpl = ''.join(
290 290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291 291
292 292 if method == 'download':
293 293 response = Response(diff)
294 294 response.content_type = 'text/plain'
295 295 response.content_disposition = (
296 296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 297 return response
298 298 elif method == 'patch':
299 299 c.diff = safe_unicode(diff)
300 300 patch = render(
301 301 'rhodecode:templates/changeset/patch_changeset.mako',
302 302 self._get_template_context(c), self.request)
303 303 response = Response(patch)
304 304 response.content_type = 'text/plain'
305 305 return response
306 306 elif method == 'raw':
307 307 response = Response(diff)
308 308 response.content_type = 'text/plain'
309 309 return response
310 310 elif method == 'show':
311 311 if len(c.commit_ranges) == 1:
312 312 html = render(
313 313 'rhodecode:templates/changeset/changeset.mako',
314 314 self._get_template_context(c), self.request)
315 315 return Response(html)
316 316 else:
317 317 c.ancestor = None
318 318 c.target_repo = self.db_repo
319 319 html = render(
320 320 'rhodecode:templates/changeset/changeset_range.mako',
321 321 self._get_template_context(c), self.request)
322 322 return Response(html)
323 323
324 324 raise HTTPBadRequest()
325 325
326 326 @LoginRequired()
327 327 @HasRepoPermissionAnyDecorator(
328 328 'repository.read', 'repository.write', 'repository.admin')
329 329 @view_config(
330 330 route_name='repo_commit', request_method='GET',
331 331 renderer=None)
332 332 def repo_commit_show(self):
333 333 commit_id = self.request.matchdict['commit_id']
334 334 return self._commit(commit_id, method='show')
335 335
336 336 @LoginRequired()
337 337 @HasRepoPermissionAnyDecorator(
338 338 'repository.read', 'repository.write', 'repository.admin')
339 339 @view_config(
340 340 route_name='repo_commit_raw', request_method='GET',
341 341 renderer=None)
342 342 @view_config(
343 343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 344 renderer=None)
345 345 def repo_commit_raw(self):
346 346 commit_id = self.request.matchdict['commit_id']
347 347 return self._commit(commit_id, method='raw')
348 348
349 349 @LoginRequired()
350 350 @HasRepoPermissionAnyDecorator(
351 351 'repository.read', 'repository.write', 'repository.admin')
352 352 @view_config(
353 353 route_name='repo_commit_patch', request_method='GET',
354 354 renderer=None)
355 355 def repo_commit_patch(self):
356 356 commit_id = self.request.matchdict['commit_id']
357 357 return self._commit(commit_id, method='patch')
358 358
359 359 @LoginRequired()
360 360 @HasRepoPermissionAnyDecorator(
361 361 'repository.read', 'repository.write', 'repository.admin')
362 362 @view_config(
363 363 route_name='repo_commit_download', request_method='GET',
364 364 renderer=None)
365 365 def repo_commit_download(self):
366 366 commit_id = self.request.matchdict['commit_id']
367 367 return self._commit(commit_id, method='download')
368 368
369 369 @LoginRequired()
370 370 @NotAnonymous()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @CSRFRequired()
374 374 @view_config(
375 375 route_name='repo_commit_comment_create', request_method='POST',
376 376 renderer='json_ext')
377 377 def repo_commit_comment_create(self):
378 378 _ = self.request.translate
379 379 commit_id = self.request.matchdict['commit_id']
380 380
381 381 c = self.load_default_context()
382 382 status = self.request.POST.get('changeset_status', None)
383 383 text = self.request.POST.get('text')
384 384 comment_type = self.request.POST.get('comment_type')
385 385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386 f_path = self.request.POST.get('f_path')
387 line_no = self.request.POST.get('line')
388 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
386 389
387 390 if status:
388 391 text = text or (_('Status change %(transition_icon)s %(status)s')
389 392 % {'transition_icon': '>',
390 393 'status': ChangesetStatus.get_status_lbl(status)})
391 394
392 395 multi_commit_ids = []
393 396 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 397 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 398 if _commit_id not in multi_commit_ids:
396 399 multi_commit_ids.append(_commit_id)
397 400
398 401 commit_ids = multi_commit_ids or [commit_id]
399 402
400 403 comment = None
401 404 for current_id in filter(None, commit_ids):
402 405 comment = CommentsModel().create(
403 406 text=text,
404 407 repo=self.db_repo.repo_id,
405 408 user=self._rhodecode_db_user.user_id,
406 409 commit_id=current_id,
407 f_path=self.request.POST.get('f_path'),
408 line_no=self.request.POST.get('line'),
410 f_path=f_path,
411 line_no=line_no,
409 412 status_change=(ChangesetStatus.get_status_lbl(status)
410 413 if status else None),
411 414 status_change_type=status,
412 415 comment_type=comment_type,
413 416 resolves_comment_id=resolves_comment_id,
414 417 auth_user=self._rhodecode_user
415 418 )
416 419 is_inline = comment.is_inline
417 420
418 421 # get status if set !
419 422 if status:
420 423 # if latest status was from pull request and it's closed
421 424 # disallow changing status !
422 425 # dont_allow_on_closed_pull_request = True !
423 426
424 427 try:
425 428 ChangesetStatusModel().set_status(
426 429 self.db_repo.repo_id,
427 430 status,
428 431 self._rhodecode_db_user.user_id,
429 432 comment,
430 433 revision=current_id,
431 434 dont_allow_on_closed_pull_request=True
432 435 )
433 436 except StatusChangeOnClosedPullRequestError:
434 437 msg = _('Changing the status of a commit associated with '
435 438 'a closed pull request is not allowed')
436 439 log.exception(msg)
437 440 h.flash(msg, category='warning')
438 441 raise HTTPFound(h.route_path(
439 442 'repo_commit', repo_name=self.db_repo_name,
440 443 commit_id=current_id))
441 444
442 445 commit = self.db_repo.get_commit(current_id)
443 446 CommentsModel().trigger_commit_comment_hook(
444 447 self.db_repo, self._rhodecode_user, 'create',
445 448 data={'comment': comment, 'commit': commit})
446 449
447 450 # finalize, commit and redirect
448 451 Session().commit()
449 452
450 data = {
451 'target_id': h.safeid(h.safe_unicode(
452 self.request.POST.get('f_path'))),
453 }
453 data = {}
454 454 if comment:
455 comment_id = comment.comment_id
456 data[comment_id] = {
457 'target_id': target_elem_id
458 }
455 459 c.co = comment
456 460 c.at_version_num = 0
457 461 rendered_comment = render(
458 462 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 463 self._get_template_context(c), self.request)
460 464
461 data.update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
465 data[comment_id].update(comment.get_dict())
466 data[comment_id].update({'rendered_text': rendered_comment})
463 467
464 468 comment_broadcast_channel = channelstream.comment_channel(
465 469 self.db_repo_name, commit_obj=commit)
466 470
467 471 comment_data = data
468 472 comment_type = 'inline' if is_inline else 'general'
469 473 channelstream.comment_channelstream_push(
470 474 self.request, comment_broadcast_channel, self._rhodecode_user,
471 475 _('posted a new {} comment').format(comment_type),
472 476 comment_data=comment_data)
473 477
474 478 return data
475 479
476 480 @LoginRequired()
477 481 @NotAnonymous()
478 482 @HasRepoPermissionAnyDecorator(
479 483 'repository.read', 'repository.write', 'repository.admin')
480 484 @CSRFRequired()
481 485 @view_config(
482 486 route_name='repo_commit_comment_preview', request_method='POST',
483 487 renderer='string', xhr=True)
484 488 def repo_commit_comment_preview(self):
485 489 # Technically a CSRF token is not needed as no state changes with this
486 490 # call. However, as this is a POST is better to have it, so automated
487 491 # tools don't flag it as potential CSRF.
488 492 # Post is required because the payload could be bigger than the maximum
489 493 # allowed by GET.
490 494
491 495 text = self.request.POST.get('text')
492 496 renderer = self.request.POST.get('renderer') or 'rst'
493 497 if text:
494 498 return h.render(text, renderer=renderer, mentions=True,
495 499 repo_name=self.db_repo_name)
496 500 return ''
497 501
498 502 @LoginRequired()
499 503 @HasRepoPermissionAnyDecorator(
500 504 'repository.read', 'repository.write', 'repository.admin')
501 505 @CSRFRequired()
502 506 @view_config(
503 507 route_name='repo_commit_comment_history_view', request_method='POST',
504 508 renderer='string', xhr=True)
505 509 def repo_commit_comment_history_view(self):
506 510 c = self.load_default_context()
507 511
508 512 comment_history_id = self.request.matchdict['comment_history_id']
509 513 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
510 514 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
511 515
512 516 if is_repo_comment:
513 517 c.comment_history = comment_history
514 518
515 519 rendered_comment = render(
516 520 'rhodecode:templates/changeset/comment_history.mako',
517 521 self._get_template_context(c)
518 522 , self.request)
519 523 return rendered_comment
520 524 else:
521 525 log.warning('No permissions for user %s to show comment_history_id: %s',
522 526 self._rhodecode_db_user, comment_history_id)
523 527 raise HTTPNotFound()
524 528
525 529 @LoginRequired()
526 530 @NotAnonymous()
527 531 @HasRepoPermissionAnyDecorator(
528 532 'repository.read', 'repository.write', 'repository.admin')
529 533 @CSRFRequired()
530 534 @view_config(
531 535 route_name='repo_commit_comment_attachment_upload', request_method='POST',
532 536 renderer='json_ext', xhr=True)
533 537 def repo_commit_comment_attachment_upload(self):
534 538 c = self.load_default_context()
535 539 upload_key = 'attachment'
536 540
537 541 file_obj = self.request.POST.get(upload_key)
538 542
539 543 if file_obj is None:
540 544 self.request.response.status = 400
541 545 return {'store_fid': None,
542 546 'access_path': None,
543 547 'error': '{} data field is missing'.format(upload_key)}
544 548
545 549 if not hasattr(file_obj, 'filename'):
546 550 self.request.response.status = 400
547 551 return {'store_fid': None,
548 552 'access_path': None,
549 553 'error': 'filename cannot be read from the data field'}
550 554
551 555 filename = file_obj.filename
552 556 file_display_name = filename
553 557
554 558 metadata = {
555 559 'user_uploaded': {'username': self._rhodecode_user.username,
556 560 'user_id': self._rhodecode_user.user_id,
557 561 'ip': self._rhodecode_user.ip_addr}}
558 562
559 563 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
560 564 allowed_extensions = [
561 565 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
562 566 '.pptx', '.txt', '.xlsx', '.zip']
563 567 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
564 568
565 569 try:
566 570 storage = store_utils.get_file_storage(self.request.registry.settings)
567 571 store_uid, metadata = storage.save_file(
568 572 file_obj.file, filename, extra_metadata=metadata,
569 573 extensions=allowed_extensions, max_filesize=max_file_size)
570 574 except FileNotAllowedException:
571 575 self.request.response.status = 400
572 576 permitted_extensions = ', '.join(allowed_extensions)
573 577 error_msg = 'File `{}` is not allowed. ' \
574 578 'Only following extensions are permitted: {}'.format(
575 579 filename, permitted_extensions)
576 580 return {'store_fid': None,
577 581 'access_path': None,
578 582 'error': error_msg}
579 583 except FileOverSizeException:
580 584 self.request.response.status = 400
581 585 limit_mb = h.format_byte_size_binary(max_file_size)
582 586 return {'store_fid': None,
583 587 'access_path': None,
584 588 'error': 'File {} is exceeding allowed limit of {}.'.format(
585 589 filename, limit_mb)}
586 590
587 591 try:
588 592 entry = FileStore.create(
589 593 file_uid=store_uid, filename=metadata["filename"],
590 594 file_hash=metadata["sha256"], file_size=metadata["size"],
591 595 file_display_name=file_display_name,
592 596 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
593 597 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
594 598 scope_repo_id=self.db_repo.repo_id
595 599 )
596 600 Session().add(entry)
597 601 Session().commit()
598 602 log.debug('Stored upload in DB as %s', entry)
599 603 except Exception:
600 604 log.exception('Failed to store file %s', filename)
601 605 self.request.response.status = 400
602 606 return {'store_fid': None,
603 607 'access_path': None,
604 608 'error': 'File {} failed to store in DB.'.format(filename)}
605 609
606 610 Session().commit()
607 611
608 612 return {
609 613 'store_fid': store_uid,
610 614 'access_path': h.route_path(
611 615 'download_file', fid=store_uid),
612 616 'fqn_access_path': h.route_url(
613 617 'download_file', fid=store_uid),
614 618 'repo_access_path': h.route_path(
615 619 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
616 620 'repo_fqn_access_path': h.route_url(
617 621 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
618 622 }
619 623
620 624 @LoginRequired()
621 625 @NotAnonymous()
622 626 @HasRepoPermissionAnyDecorator(
623 627 'repository.read', 'repository.write', 'repository.admin')
624 628 @CSRFRequired()
625 629 @view_config(
626 630 route_name='repo_commit_comment_delete', request_method='POST',
627 631 renderer='json_ext')
628 632 def repo_commit_comment_delete(self):
629 633 commit_id = self.request.matchdict['commit_id']
630 634 comment_id = self.request.matchdict['comment_id']
631 635
632 636 comment = ChangesetComment.get_or_404(comment_id)
633 637 if not comment:
634 638 log.debug('Comment with id:%s not found, skipping', comment_id)
635 639 # comment already deleted in another call probably
636 640 return True
637 641
638 642 if comment.immutable:
639 643 # don't allow deleting comments that are immutable
640 644 raise HTTPForbidden()
641 645
642 646 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
643 647 super_admin = h.HasPermissionAny('hg.admin')()
644 648 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
645 649 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
646 650 comment_repo_admin = is_repo_admin and is_repo_comment
647 651
648 652 if super_admin or comment_owner or comment_repo_admin:
649 653 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
650 654 Session().commit()
651 655 return True
652 656 else:
653 657 log.warning('No permissions for user %s to delete comment_id: %s',
654 658 self._rhodecode_db_user, comment_id)
655 659 raise HTTPNotFound()
656 660
657 661 @LoginRequired()
658 662 @NotAnonymous()
659 663 @HasRepoPermissionAnyDecorator(
660 664 'repository.read', 'repository.write', 'repository.admin')
661 665 @CSRFRequired()
662 666 @view_config(
663 667 route_name='repo_commit_comment_edit', request_method='POST',
664 668 renderer='json_ext')
665 669 def repo_commit_comment_edit(self):
666 670 self.load_default_context()
667 671
668 672 comment_id = self.request.matchdict['comment_id']
669 673 comment = ChangesetComment.get_or_404(comment_id)
670 674
671 675 if comment.immutable:
672 676 # don't allow deleting comments that are immutable
673 677 raise HTTPForbidden()
674 678
675 679 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
676 680 super_admin = h.HasPermissionAny('hg.admin')()
677 681 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
678 682 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
679 683 comment_repo_admin = is_repo_admin and is_repo_comment
680 684
681 685 if super_admin or comment_owner or comment_repo_admin:
682 686 text = self.request.POST.get('text')
683 687 version = self.request.POST.get('version')
684 688 if text == comment.text:
685 689 log.warning(
686 690 'Comment(repo): '
687 691 'Trying to create new version '
688 692 'with the same comment body {}'.format(
689 693 comment_id,
690 694 )
691 695 )
692 696 raise HTTPNotFound()
693 697
694 698 if version.isdigit():
695 699 version = int(version)
696 700 else:
697 701 log.warning(
698 702 'Comment(repo): Wrong version type {} {} '
699 703 'for comment {}'.format(
700 704 version,
701 705 type(version),
702 706 comment_id,
703 707 )
704 708 )
705 709 raise HTTPNotFound()
706 710
707 711 try:
708 712 comment_history = CommentsModel().edit(
709 713 comment_id=comment_id,
710 714 text=text,
711 715 auth_user=self._rhodecode_user,
712 716 version=version,
713 717 )
714 718 except CommentVersionMismatch:
715 719 raise HTTPConflict()
716 720
717 721 if not comment_history:
718 722 raise HTTPNotFound()
719 723
720 724 commit_id = self.request.matchdict['commit_id']
721 725 commit = self.db_repo.get_commit(commit_id)
722 726 CommentsModel().trigger_commit_comment_hook(
723 727 self.db_repo, self._rhodecode_user, 'edit',
724 728 data={'comment': comment, 'commit': commit})
725 729
726 730 Session().commit()
727 731 return {
728 732 'comment_history_id': comment_history.comment_history_id,
729 733 'comment_id': comment.comment_id,
730 734 'comment_version': comment_history.version,
731 735 'comment_author_username': comment_history.author.username,
732 736 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
733 737 'comment_created_on': h.age_component(comment_history.created_on,
734 738 time_is_local=True),
735 739 }
736 740 else:
737 741 log.warning('No permissions for user %s to edit comment_id: %s',
738 742 self._rhodecode_db_user, comment_id)
739 743 raise HTTPNotFound()
740 744
741 745 @LoginRequired()
742 746 @HasRepoPermissionAnyDecorator(
743 747 'repository.read', 'repository.write', 'repository.admin')
744 748 @view_config(
745 749 route_name='repo_commit_data', request_method='GET',
746 750 renderer='json_ext', xhr=True)
747 751 def repo_commit_data(self):
748 752 commit_id = self.request.matchdict['commit_id']
749 753 self.load_default_context()
750 754
751 755 try:
752 756 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
753 757 except CommitDoesNotExistError as e:
754 758 return EmptyCommit(message=str(e))
755 759
756 760 @LoginRequired()
757 761 @HasRepoPermissionAnyDecorator(
758 762 'repository.read', 'repository.write', 'repository.admin')
759 763 @view_config(
760 764 route_name='repo_commit_children', request_method='GET',
761 765 renderer='json_ext', xhr=True)
762 766 def repo_commit_children(self):
763 767 commit_id = self.request.matchdict['commit_id']
764 768 self.load_default_context()
765 769
766 770 try:
767 771 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
768 772 children = commit.children
769 773 except CommitDoesNotExistError:
770 774 children = []
771 775
772 776 result = {"results": children}
773 777 return result
774 778
775 779 @LoginRequired()
776 780 @HasRepoPermissionAnyDecorator(
777 781 'repository.read', 'repository.write', 'repository.admin')
778 782 @view_config(
779 783 route_name='repo_commit_parents', request_method='GET',
780 784 renderer='json_ext')
781 785 def repo_commit_parents(self):
782 786 commit_id = self.request.matchdict['commit_id']
783 787 self.load_default_context()
784 788
785 789 try:
786 790 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 791 parents = commit.parents
788 792 except CommitDoesNotExistError:
789 793 parents = []
790 794 result = {"results": parents}
791 795 return result
@@ -1,1826 +1,1856 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 83 statuses=statuses, offset=start, length=limit,
84 84 order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 87 opened_by=opened_by)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by,
93 93 order_dir=order_dir)
94 94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 96 statuses=statuses, opened_by=opened_by)
97 97 else:
98 98 pull_requests = PullRequestModel().get_all(
99 99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 100 statuses=statuses, offset=start, length=limit,
101 101 order_by=order_by, order_dir=order_dir)
102 102 pull_requests_total_count = PullRequestModel().count_all(
103 103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 104 opened_by=opened_by)
105 105
106 106 data = []
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 110 self.db_repo.repo_id, pull_request=pr, count_only=True)
111 111
112 112 data.append({
113 113 'name': _render('pullrequest_name',
114 114 pr.pull_request_id, pr.pull_request_state,
115 115 pr.work_in_progress, pr.target_repo.repo_name,
116 116 short=True),
117 117 'name_raw': pr.pull_request_id,
118 118 'status': _render('pullrequest_status',
119 119 pr.calculated_review_status()),
120 120 'title': _render('pullrequest_title', pr.title, pr.description),
121 121 'description': h.escape(pr.description),
122 122 'updated_on': _render('pullrequest_updated_on',
123 123 h.datetime_to_time(pr.updated_on)),
124 124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 125 'created_on': _render('pullrequest_updated_on',
126 126 h.datetime_to_time(pr.created_on)),
127 127 'created_on_raw': h.datetime_to_time(pr.created_on),
128 128 'state': pr.pull_request_state,
129 129 'author': _render('pullrequest_author',
130 130 pr.author.full_contact, ),
131 131 'author_raw': pr.author.full_name,
132 132 'comments': _render('pullrequest_comments', comments_count),
133 133 'comments_raw': comments_count,
134 134 'closed': pr.is_closed(),
135 135 })
136 136
137 137 data = ({
138 138 'draw': draw,
139 139 'data': data,
140 140 'recordsTotal': pull_requests_total_count,
141 141 'recordsFiltered': pull_requests_total_count,
142 142 })
143 143 return data
144 144
145 145 @LoginRequired()
146 146 @HasRepoPermissionAnyDecorator(
147 147 'repository.read', 'repository.write', 'repository.admin')
148 148 @view_config(
149 149 route_name='pullrequest_show_all', request_method='GET',
150 150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
151 151 def pull_request_list(self):
152 152 c = self.load_default_context()
153 153
154 154 req_get = self.request.GET
155 155 c.source = str2bool(req_get.get('source'))
156 156 c.closed = str2bool(req_get.get('closed'))
157 157 c.my = str2bool(req_get.get('my'))
158 158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
159 159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
160 160
161 161 c.active = 'open'
162 162 if c.my:
163 163 c.active = 'my'
164 164 if c.closed:
165 165 c.active = 'closed'
166 166 if c.awaiting_review and not c.source:
167 167 c.active = 'awaiting'
168 168 if c.source and not c.awaiting_review:
169 169 c.active = 'source'
170 170 if c.awaiting_my_review:
171 171 c.active = 'awaiting_my'
172 172
173 173 return self._get_template_context(c)
174 174
175 175 @LoginRequired()
176 176 @HasRepoPermissionAnyDecorator(
177 177 'repository.read', 'repository.write', 'repository.admin')
178 178 @view_config(
179 179 route_name='pullrequest_show_all_data', request_method='GET',
180 180 renderer='json_ext', xhr=True)
181 181 def pull_request_list_data(self):
182 182 self.load_default_context()
183 183
184 184 # additional filters
185 185 req_get = self.request.GET
186 186 source = str2bool(req_get.get('source'))
187 187 closed = str2bool(req_get.get('closed'))
188 188 my = str2bool(req_get.get('my'))
189 189 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191 191
192 192 filter_type = 'awaiting_review' if awaiting_review \
193 193 else 'awaiting_my_review' if awaiting_my_review \
194 194 else None
195 195
196 196 opened_by = None
197 197 if my:
198 198 opened_by = [self._rhodecode_user.user_id]
199 199
200 200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 201 if closed:
202 202 statuses = [PullRequest.STATUS_CLOSED]
203 203
204 204 data = self._get_pull_requests_list(
205 205 repo_name=self.db_repo_name, source=source,
206 206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207 207
208 208 return data
209 209
210 210 def _is_diff_cache_enabled(self, target_repo):
211 211 caching_enabled = self._get_general_setting(
212 212 target_repo, 'rhodecode_diff_cache')
213 213 log.debug('Diff caching enabled: %s', caching_enabled)
214 214 return caching_enabled
215 215
216 216 def _get_diffset(self, source_repo_name, source_repo,
217 217 ancestor_commit,
218 218 source_ref_id, target_ref_id,
219 219 target_commit, source_commit, diff_limit, file_limit,
220 220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221 221
222 222 if use_ancestor:
223 223 # we might want to not use it for versions
224 224 target_ref_id = ancestor_commit.raw_id
225 225
226 226 vcs_diff = PullRequestModel().get_diff(
227 227 source_repo, source_ref_id, target_ref_id,
228 228 hide_whitespace_changes, diff_context)
229 229
230 230 diff_processor = diffs.DiffProcessor(
231 231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 232 file_limit=file_limit, show_full_diff=fulldiff)
233 233
234 234 _parsed = diff_processor.prepare()
235 235
236 236 diffset = codeblocks.DiffSet(
237 237 repo_name=self.db_repo_name,
238 238 source_repo_name=source_repo_name,
239 239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
240 240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
241 241 )
242 242 diffset = self.path_filter.render_patchset_filtered(
243 243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
244 244
245 245 return diffset
246 246
247 247 def _get_range_diffset(self, source_scm, source_repo,
248 248 commit1, commit2, diff_limit, file_limit,
249 249 fulldiff, hide_whitespace_changes, diff_context):
250 250 vcs_diff = source_scm.get_diff(
251 251 commit1, commit2,
252 252 ignore_whitespace=hide_whitespace_changes,
253 253 context=diff_context)
254 254
255 255 diff_processor = diffs.DiffProcessor(
256 256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 257 file_limit=file_limit, show_full_diff=fulldiff)
258 258
259 259 _parsed = diff_processor.prepare()
260 260
261 261 diffset = codeblocks.DiffSet(
262 262 repo_name=source_repo.repo_name,
263 263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265 265
266 266 diffset = self.path_filter.render_patchset_filtered(
267 267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268 268
269 269 return diffset
270 270
271 271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 272 comments_model = CommentsModel()
273 273
274 274 # GENERAL COMMENTS with versions #
275 275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 276 q = q.order_by(ChangesetComment.comment_id.asc())
277 277 if not include_drafts:
278 278 q = q.filter(ChangesetComment.draft == false())
279 279 general_comments = q
280 280
281 281 # pick comments we want to render at current version
282 282 c.comment_versions = comments_model.aggregate_comments(
283 283 general_comments, versions, c.at_version_num)
284 284
285 285 # INLINE COMMENTS with versions #
286 286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 287 q = q.order_by(ChangesetComment.comment_id.asc())
288 288 if not include_drafts:
289 289 q = q.filter(ChangesetComment.draft == false())
290 290 inline_comments = q
291 291
292 292 c.inline_versions = comments_model.aggregate_comments(
293 293 inline_comments, versions, c.at_version_num, inline=True)
294 294
295 295 # Comments inline+general
296 296 if c.at_version:
297 297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 298 c.comments = c.comment_versions[c.at_version_num]['display']
299 299 else:
300 300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 301 c.comments = c.comment_versions[c.at_version_num]['until']
302 302
303 303 return general_comments, inline_comments
304 304
305 305 @LoginRequired()
306 306 @HasRepoPermissionAnyDecorator(
307 307 'repository.read', 'repository.write', 'repository.admin')
308 308 @view_config(
309 309 route_name='pullrequest_show', request_method='GET',
310 310 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
311 311 def pull_request_show(self):
312 312 _ = self.request.translate
313 313 c = self.load_default_context()
314 314
315 315 pull_request = PullRequest.get_or_404(
316 316 self.request.matchdict['pull_request_id'])
317 317 pull_request_id = pull_request.pull_request_id
318 318
319 319 c.state_progressing = pull_request.is_state_changing()
320 320 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
321 321
322 322 _new_state = {
323 323 'created': PullRequest.STATE_CREATED,
324 324 }.get(self.request.GET.get('force_state'))
325 325
326 326 if c.is_super_admin and _new_state:
327 327 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
328 328 h.flash(
329 329 _('Pull Request state was force changed to `{}`').format(_new_state),
330 330 category='success')
331 331 Session().commit()
332 332
333 333 raise HTTPFound(h.route_path(
334 334 'pullrequest_show', repo_name=self.db_repo_name,
335 335 pull_request_id=pull_request_id))
336 336
337 337 version = self.request.GET.get('version')
338 338 from_version = self.request.GET.get('from_version') or version
339 339 merge_checks = self.request.GET.get('merge_checks')
340 340 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
341 341 force_refresh = str2bool(self.request.GET.get('force_refresh'))
342 342 c.range_diff_on = self.request.GET.get('range-diff') == "1"
343 343
344 344 # fetch global flags of ignore ws or context lines
345 345 diff_context = diffs.get_diff_context(self.request)
346 346 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
347 347
348 348 (pull_request_latest,
349 349 pull_request_at_ver,
350 350 pull_request_display_obj,
351 351 at_version) = PullRequestModel().get_pr_version(
352 352 pull_request_id, version=version)
353 353
354 354 pr_closed = pull_request_latest.is_closed()
355 355
356 356 if pr_closed and (version or from_version):
357 357 # not allow to browse versions for closed PR
358 358 raise HTTPFound(h.route_path(
359 359 'pullrequest_show', repo_name=self.db_repo_name,
360 360 pull_request_id=pull_request_id))
361 361
362 362 versions = pull_request_display_obj.versions()
363 363 # used to store per-commit range diffs
364 364 c.changes = collections.OrderedDict()
365 365
366 366 c.at_version = at_version
367 367 c.at_version_num = (at_version
368 368 if at_version and at_version != PullRequest.LATEST_VER
369 369 else None)
370 370
371 371 c.at_version_index = ChangesetComment.get_index_from_version(
372 372 c.at_version_num, versions)
373 373
374 374 (prev_pull_request_latest,
375 375 prev_pull_request_at_ver,
376 376 prev_pull_request_display_obj,
377 377 prev_at_version) = PullRequestModel().get_pr_version(
378 378 pull_request_id, version=from_version)
379 379
380 380 c.from_version = prev_at_version
381 381 c.from_version_num = (prev_at_version
382 382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 383 else None)
384 384 c.from_version_index = ChangesetComment.get_index_from_version(
385 385 c.from_version_num, versions)
386 386
387 387 # define if we're in COMPARE mode or VIEW at version mode
388 388 compare = at_version != prev_at_version
389 389
390 390 # pull_requests repo_name we opened it against
391 391 # ie. target_repo must match
392 392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 393 log.warning('Mismatch between the current repo: %s, and target %s',
394 394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 395 raise HTTPNotFound()
396 396
397 397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398 398
399 399 c.pull_request = pull_request_display_obj
400 400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 401 c.pull_request_latest = pull_request_latest
402 402
403 403 # inject latest version
404 404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 405 c.versions = versions + [latest_ver]
406 406
407 407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 408 c.allowed_to_change_status = False
409 409 c.allowed_to_update = False
410 410 c.allowed_to_merge = False
411 411 c.allowed_to_delete = False
412 412 c.allowed_to_comment = False
413 413 c.allowed_to_close = False
414 414 else:
415 415 can_change_status = PullRequestModel().check_user_change_status(
416 416 pull_request_at_ver, self._rhodecode_user)
417 417 c.allowed_to_change_status = can_change_status and not pr_closed
418 418
419 419 c.allowed_to_update = PullRequestModel().check_user_update(
420 420 pull_request_latest, self._rhodecode_user) and not pr_closed
421 421 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 423 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 425 c.allowed_to_comment = not pr_closed
426 426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427 427
428 428 c.forbid_adding_reviewers = False
429 429 c.forbid_author_to_review = False
430 430 c.forbid_commit_author_to_review = False
431 431
432 432 if pull_request_latest.reviewer_data and \
433 433 'rules' in pull_request_latest.reviewer_data:
434 434 rules = pull_request_latest.reviewer_data['rules'] or {}
435 435 try:
436 436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 437 c.forbid_author_to_review = rules.get('forbid_author_to_review')
438 438 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
439 439 except Exception:
440 440 pass
441 441
442 442 # check merge capabilities
443 443 _merge_check = MergeCheck.validate(
444 444 pull_request_latest, auth_user=self._rhodecode_user,
445 445 translator=self.request.translate,
446 446 force_shadow_repo_refresh=force_refresh)
447 447
448 448 c.pr_merge_errors = _merge_check.error_details
449 449 c.pr_merge_possible = not _merge_check.failed
450 450 c.pr_merge_message = _merge_check.merge_msg
451 451 c.pr_merge_source_commit = _merge_check.source_commit
452 452 c.pr_merge_target_commit = _merge_check.target_commit
453 453
454 454 c.pr_merge_info = MergeCheck.get_merge_conditions(
455 455 pull_request_latest, translator=self.request.translate)
456 456
457 457 c.pull_request_review_status = _merge_check.review_status
458 458 if merge_checks:
459 459 self.request.override_renderer = \
460 460 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
461 461 return self._get_template_context(c)
462 462
463 463 c.reviewers_count = pull_request.reviewers_count
464 464 c.observers_count = pull_request.observers_count
465 465
466 466 # reviewers and statuses
467 467 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
468 468 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
469 469 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
470 470
471 471 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
472 472 member_reviewer = h.reviewer_as_json(
473 473 member, reasons=reasons, mandatory=mandatory,
474 474 role=review_obj.role,
475 475 user_group=review_obj.rule_user_group_data()
476 476 )
477 477
478 478 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
479 479 member_reviewer['review_status'] = current_review_status
480 480 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
481 481 member_reviewer['allowed_to_update'] = c.allowed_to_update
482 482 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
483 483
484 484 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
485 485
486 486 for observer_obj, member in pull_request_at_ver.observers():
487 487 member_observer = h.reviewer_as_json(
488 488 member, reasons=[], mandatory=False,
489 489 role=observer_obj.role,
490 490 user_group=observer_obj.rule_user_group_data()
491 491 )
492 492 member_observer['allowed_to_update'] = c.allowed_to_update
493 493 c.pull_request_set_observers_data_json['observers'].append(member_observer)
494 494
495 495 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
496 496
497 497 general_comments, inline_comments = \
498 498 self.register_comments_vars(c, pull_request_latest, versions)
499 499
500 500 # TODOs
501 501 c.unresolved_comments = CommentsModel() \
502 502 .get_pull_request_unresolved_todos(pull_request_latest)
503 503 c.resolved_comments = CommentsModel() \
504 504 .get_pull_request_resolved_todos(pull_request_latest)
505 505
506 506 # if we use version, then do not show later comments
507 507 # than current version
508 508 display_inline_comments = collections.defaultdict(
509 509 lambda: collections.defaultdict(list))
510 510 for co in inline_comments:
511 511 if c.at_version_num:
512 512 # pick comments that are at least UPTO given version, so we
513 513 # don't render comments for higher version
514 514 should_render = co.pull_request_version_id and \
515 515 co.pull_request_version_id <= c.at_version_num
516 516 else:
517 517 # showing all, for 'latest'
518 518 should_render = True
519 519
520 520 if should_render:
521 521 display_inline_comments[co.f_path][co.line_no].append(co)
522 522
523 523 # load diff data into template context, if we use compare mode then
524 524 # diff is calculated based on changes between versions of PR
525 525
526 526 source_repo = pull_request_at_ver.source_repo
527 527 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528 528
529 529 target_repo = pull_request_at_ver.target_repo
530 530 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531 531
532 532 if compare:
533 533 # in compare switch the diff base to latest commit from prev version
534 534 target_ref_id = prev_pull_request_display_obj.revisions[0]
535 535
536 536 # despite opening commits for bookmarks/branches/tags, we always
537 537 # convert this to rev to prevent changes after bookmark or branch change
538 538 c.source_ref_type = 'rev'
539 539 c.source_ref = source_ref_id
540 540
541 541 c.target_ref_type = 'rev'
542 542 c.target_ref = target_ref_id
543 543
544 544 c.source_repo = source_repo
545 545 c.target_repo = target_repo
546 546
547 547 c.commit_ranges = []
548 548 source_commit = EmptyCommit()
549 549 target_commit = EmptyCommit()
550 550 c.missing_requirements = False
551 551
552 552 source_scm = source_repo.scm_instance()
553 553 target_scm = target_repo.scm_instance()
554 554
555 555 shadow_scm = None
556 556 try:
557 557 shadow_scm = pull_request_latest.get_shadow_repo()
558 558 except Exception:
559 559 log.debug('Failed to get shadow repo', exc_info=True)
560 560 # try first the existing source_repo, and then shadow
561 561 # repo if we can obtain one
562 562 commits_source_repo = source_scm
563 563 if shadow_scm:
564 564 commits_source_repo = shadow_scm
565 565
566 566 c.commits_source_repo = commits_source_repo
567 567 c.ancestor = None # set it to None, to hide it from PR view
568 568
569 569 # empty version means latest, so we keep this to prevent
570 570 # double caching
571 571 version_normalized = version or PullRequest.LATEST_VER
572 572 from_version_normalized = from_version or PullRequest.LATEST_VER
573 573
574 574 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 575 cache_file_path = diff_cache_exist(
576 576 cache_path, 'pull_request', pull_request_id, version_normalized,
577 577 from_version_normalized, source_ref_id, target_ref_id,
578 578 hide_whitespace_changes, diff_context, c.fulldiff)
579 579
580 580 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 581 force_recache = self.get_recache_flag()
582 582
583 583 cached_diff = None
584 584 if caching_enabled:
585 585 cached_diff = load_cached_diff(cache_file_path)
586 586
587 587 has_proper_commit_cache = (
588 588 cached_diff and cached_diff.get('commits')
589 589 and len(cached_diff.get('commits', [])) == 5
590 590 and cached_diff.get('commits')[0]
591 591 and cached_diff.get('commits')[3])
592 592
593 593 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 594 diff_commit_cache = \
595 595 (ancestor_commit, commit_cache, missing_requirements,
596 596 source_commit, target_commit) = cached_diff['commits']
597 597 else:
598 598 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 599 # merge errors resulting in potentially hidden commits in the shadow repo.
600 600 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 601 and _merge_check.merge_response
602 602 maybe_unreachable = maybe_unreachable \
603 603 and _merge_check.merge_response.metadata.get('unresolved_files')
604 604 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 605 diff_commit_cache = \
606 606 (ancestor_commit, commit_cache, missing_requirements,
607 607 source_commit, target_commit) = self.get_commits(
608 608 commits_source_repo,
609 609 pull_request_at_ver,
610 610 source_commit,
611 611 source_ref_id,
612 612 source_scm,
613 613 target_commit,
614 614 target_ref_id,
615 615 target_scm,
616 616 maybe_unreachable=maybe_unreachable)
617 617
618 618 # register our commit range
619 619 for comm in commit_cache.values():
620 620 c.commit_ranges.append(comm)
621 621
622 622 c.missing_requirements = missing_requirements
623 623 c.ancestor_commit = ancestor_commit
624 624 c.statuses = source_repo.statuses(
625 625 [x.raw_id for x in c.commit_ranges])
626 626
627 627 # auto collapse if we have more than limit
628 628 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 629 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 630 c.compare_mode = compare
631 631
632 632 # diff_limit is the old behavior, will cut off the whole diff
633 633 # if the limit is applied otherwise will just hide the
634 634 # big files from the front-end
635 635 diff_limit = c.visual.cut_off_limit_diff
636 636 file_limit = c.visual.cut_off_limit_file
637 637
638 638 c.missing_commits = False
639 639 if (c.missing_requirements
640 640 or isinstance(source_commit, EmptyCommit)
641 641 or source_commit == target_commit):
642 642
643 643 c.missing_commits = True
644 644 else:
645 645 c.inline_comments = display_inline_comments
646 646
647 647 use_ancestor = True
648 648 if from_version_normalized != version_normalized:
649 649 use_ancestor = False
650 650
651 651 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 652 if not force_recache and has_proper_diff_cache:
653 653 c.diffset = cached_diff['diff']
654 654 else:
655 655 try:
656 656 c.diffset = self._get_diffset(
657 657 c.source_repo.repo_name, commits_source_repo,
658 658 c.ancestor_commit,
659 659 source_ref_id, target_ref_id,
660 660 target_commit, source_commit,
661 661 diff_limit, file_limit, c.fulldiff,
662 662 hide_whitespace_changes, diff_context,
663 663 use_ancestor=use_ancestor
664 664 )
665 665
666 666 # save cached diff
667 667 if caching_enabled:
668 668 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 669 except CommitDoesNotExistError:
670 670 log.exception('Failed to generate diffset')
671 671 c.missing_commits = True
672 672
673 673 if not c.missing_commits:
674 674
675 675 c.limited_diff = c.diffset.limited_diff
676 676
677 677 # calculate removed files that are bound to comments
678 678 comment_deleted_files = [
679 679 fname for fname in display_inline_comments
680 680 if fname not in c.diffset.file_stats]
681 681
682 682 c.deleted_files_comments = collections.defaultdict(dict)
683 683 for fname, per_line_comments in display_inline_comments.items():
684 684 if fname in comment_deleted_files:
685 685 c.deleted_files_comments[fname]['stats'] = 0
686 686 c.deleted_files_comments[fname]['comments'] = list()
687 687 for lno, comments in per_line_comments.items():
688 688 c.deleted_files_comments[fname]['comments'].extend(comments)
689 689
690 690 # maybe calculate the range diff
691 691 if c.range_diff_on:
692 692 # TODO(marcink): set whitespace/context
693 693 context_lcl = 3
694 694 ign_whitespace_lcl = False
695 695
696 696 for commit in c.commit_ranges:
697 697 commit2 = commit
698 698 commit1 = commit.first_parent
699 699
700 700 range_diff_cache_file_path = diff_cache_exist(
701 701 cache_path, 'diff', commit.raw_id,
702 702 ign_whitespace_lcl, context_lcl, c.fulldiff)
703 703
704 704 cached_diff = None
705 705 if caching_enabled:
706 706 cached_diff = load_cached_diff(range_diff_cache_file_path)
707 707
708 708 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 709 if not force_recache and has_proper_diff_cache:
710 710 diffset = cached_diff['diff']
711 711 else:
712 712 diffset = self._get_range_diffset(
713 713 commits_source_repo, source_repo,
714 714 commit1, commit2, diff_limit, file_limit,
715 715 c.fulldiff, ign_whitespace_lcl, context_lcl
716 716 )
717 717
718 718 # save cached diff
719 719 if caching_enabled:
720 720 cache_diff(range_diff_cache_file_path, diffset, None)
721 721
722 722 c.changes[commit.raw_id] = diffset
723 723
724 724 # this is a hack to properly display links, when creating PR, the
725 725 # compare view and others uses different notation, and
726 726 # compare_commits.mako renders links based on the target_repo.
727 727 # We need to swap that here to generate it properly on the html side
728 728 c.target_repo = c.source_repo
729 729
730 730 c.commit_statuses = ChangesetStatus.STATUSES
731 731
732 732 c.show_version_changes = not pr_closed
733 733 if c.show_version_changes:
734 734 cur_obj = pull_request_at_ver
735 735 prev_obj = prev_pull_request_at_ver
736 736
737 737 old_commit_ids = prev_obj.revisions
738 738 new_commit_ids = cur_obj.revisions
739 739 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 740 old_commit_ids, new_commit_ids)
741 741 c.commit_changes_summary = commit_changes
742 742
743 743 # calculate the diff for commits between versions
744 744 c.commit_changes = []
745 745
746 746 def mark(cs, fw):
747 747 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748 748
749 749 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 750 + mark(commit_changes.removed, 'r') \
751 751 + mark(commit_changes.common, 'c'):
752 752
753 753 if raw_id in commit_cache:
754 754 commit = commit_cache[raw_id]
755 755 else:
756 756 try:
757 757 commit = commits_source_repo.get_commit(raw_id)
758 758 except CommitDoesNotExistError:
759 759 # in case we fail extracting still use "dummy" commit
760 760 # for display in commit diff
761 761 commit = h.AttributeDict(
762 762 {'raw_id': raw_id,
763 763 'message': 'EMPTY or MISSING COMMIT'})
764 764 c.commit_changes.append([c_type, commit])
765 765
766 766 # current user review statuses for each version
767 767 c.review_versions = {}
768 768 is_reviewer = PullRequestModel().is_user_reviewer(
769 769 pull_request, self._rhodecode_user)
770 770 if is_reviewer:
771 771 for co in general_comments:
772 772 if co.author.user_id == self._rhodecode_user.user_id:
773 773 status = co.status_change
774 774 if status:
775 775 _ver_pr = status[0].comment.pull_request_version_id
776 776 c.review_versions[_ver_pr] = status[0]
777 777
778 778 return self._get_template_context(c)
779 779
780 780 def get_commits(
781 781 self, commits_source_repo, pull_request_at_ver, source_commit,
782 782 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 783 maybe_unreachable=False):
784 784
785 785 commit_cache = collections.OrderedDict()
786 786 missing_requirements = False
787 787
788 788 try:
789 789 pre_load = ["author", "date", "message", "branch", "parents"]
790 790
791 791 pull_request_commits = pull_request_at_ver.revisions
792 792 log.debug('Loading %s commits from %s',
793 793 len(pull_request_commits), commits_source_repo)
794 794
795 795 for rev in pull_request_commits:
796 796 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 797 maybe_unreachable=maybe_unreachable)
798 798 commit_cache[comm.raw_id] = comm
799 799
800 800 # Order here matters, we first need to get target, and then
801 801 # the source
802 802 target_commit = commits_source_repo.get_commit(
803 803 commit_id=safe_str(target_ref_id))
804 804
805 805 source_commit = commits_source_repo.get_commit(
806 806 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 807 except CommitDoesNotExistError:
808 808 log.warning('Failed to get commit from `{}` repo'.format(
809 809 commits_source_repo), exc_info=True)
810 810 except RepositoryRequirementError:
811 811 log.warning('Failed to get all required data from repo', exc_info=True)
812 812 missing_requirements = True
813 813
814 814 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815 815
816 816 try:
817 817 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 818 except Exception:
819 819 ancestor_commit = None
820 820
821 821 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822 822
823 823 def assure_not_empty_repo(self):
824 824 _ = self.request.translate
825 825
826 826 try:
827 827 self.db_repo.scm_instance().get_commit()
828 828 except EmptyRepositoryError:
829 829 h.flash(h.literal(_('There are no commits yet')),
830 830 category='warning')
831 831 raise HTTPFound(
832 832 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833 833
834 834 @LoginRequired()
835 835 @NotAnonymous()
836 836 @HasRepoPermissionAnyDecorator(
837 837 'repository.read', 'repository.write', 'repository.admin')
838 838 @view_config(
839 839 route_name='pullrequest_new', request_method='GET',
840 840 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
841 841 def pull_request_new(self):
842 842 _ = self.request.translate
843 843 c = self.load_default_context()
844 844
845 845 self.assure_not_empty_repo()
846 846 source_repo = self.db_repo
847 847
848 848 commit_id = self.request.GET.get('commit')
849 849 branch_ref = self.request.GET.get('branch')
850 850 bookmark_ref = self.request.GET.get('bookmark')
851 851
852 852 try:
853 853 source_repo_data = PullRequestModel().generate_repo_data(
854 854 source_repo, commit_id=commit_id,
855 855 branch=branch_ref, bookmark=bookmark_ref,
856 856 translator=self.request.translate)
857 857 except CommitDoesNotExistError as e:
858 858 log.exception(e)
859 859 h.flash(_('Commit does not exist'), 'error')
860 860 raise HTTPFound(
861 861 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
862 862
863 863 default_target_repo = source_repo
864 864
865 865 if source_repo.parent and c.has_origin_repo_read_perm:
866 866 parent_vcs_obj = source_repo.parent.scm_instance()
867 867 if parent_vcs_obj and not parent_vcs_obj.is_empty():
868 868 # change default if we have a parent repo
869 869 default_target_repo = source_repo.parent
870 870
871 871 target_repo_data = PullRequestModel().generate_repo_data(
872 872 default_target_repo, translator=self.request.translate)
873 873
874 874 selected_source_ref = source_repo_data['refs']['selected_ref']
875 875 title_source_ref = ''
876 876 if selected_source_ref:
877 877 title_source_ref = selected_source_ref.split(':', 2)[1]
878 878 c.default_title = PullRequestModel().generate_pullrequest_title(
879 879 source=source_repo.repo_name,
880 880 source_ref=title_source_ref,
881 881 target=default_target_repo.repo_name
882 882 )
883 883
884 884 c.default_repo_data = {
885 885 'source_repo_name': source_repo.repo_name,
886 886 'source_refs_json': json.dumps(source_repo_data),
887 887 'target_repo_name': default_target_repo.repo_name,
888 888 'target_refs_json': json.dumps(target_repo_data),
889 889 }
890 890 c.default_source_ref = selected_source_ref
891 891
892 892 return self._get_template_context(c)
893 893
894 894 @LoginRequired()
895 895 @NotAnonymous()
896 896 @HasRepoPermissionAnyDecorator(
897 897 'repository.read', 'repository.write', 'repository.admin')
898 898 @view_config(
899 899 route_name='pullrequest_repo_refs', request_method='GET',
900 900 renderer='json_ext', xhr=True)
901 901 def pull_request_repo_refs(self):
902 902 self.load_default_context()
903 903 target_repo_name = self.request.matchdict['target_repo_name']
904 904 repo = Repository.get_by_repo_name(target_repo_name)
905 905 if not repo:
906 906 raise HTTPNotFound()
907 907
908 908 target_perm = HasRepoPermissionAny(
909 909 'repository.read', 'repository.write', 'repository.admin')(
910 910 target_repo_name)
911 911 if not target_perm:
912 912 raise HTTPNotFound()
913 913
914 914 return PullRequestModel().generate_repo_data(
915 915 repo, translator=self.request.translate)
916 916
917 917 @LoginRequired()
918 918 @NotAnonymous()
919 919 @HasRepoPermissionAnyDecorator(
920 920 'repository.read', 'repository.write', 'repository.admin')
921 921 @view_config(
922 922 route_name='pullrequest_repo_targets', request_method='GET',
923 923 renderer='json_ext', xhr=True)
924 924 def pullrequest_repo_targets(self):
925 925 _ = self.request.translate
926 926 filter_query = self.request.GET.get('query')
927 927
928 928 # get the parents
929 929 parent_target_repos = []
930 930 if self.db_repo.parent:
931 931 parents_query = Repository.query() \
932 932 .order_by(func.length(Repository.repo_name)) \
933 933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934 934
935 935 if filter_query:
936 936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 937 parents_query = parents_query.filter(
938 938 Repository.repo_name.ilike(ilike_expression))
939 939 parents = parents_query.limit(20).all()
940 940
941 941 for parent in parents:
942 942 parent_vcs_obj = parent.scm_instance()
943 943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 944 parent_target_repos.append(parent)
945 945
946 946 # get other forks, and repo itself
947 947 query = Repository.query() \
948 948 .order_by(func.length(Repository.repo_name)) \
949 949 .filter(
950 950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 952 ) \
953 953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954 954
955 955 if filter_query:
956 956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958 958
959 959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 960 target_repos = query.limit(limit).all()
961 961
962 962 all_target_repos = target_repos + parent_target_repos
963 963
964 964 repos = []
965 965 # This checks permissions to the repositories
966 966 for obj in ScmModel().get_repos(all_target_repos):
967 967 repos.append({
968 968 'id': obj['name'],
969 969 'text': obj['name'],
970 970 'type': 'repo',
971 971 'repo_id': obj['dbrepo']['repo_id'],
972 972 'repo_type': obj['dbrepo']['repo_type'],
973 973 'private': obj['dbrepo']['private'],
974 974
975 975 })
976 976
977 977 data = {
978 978 'more': False,
979 979 'results': [{
980 980 'text': _('Repositories'),
981 981 'children': repos
982 982 }] if repos else []
983 983 }
984 984 return data
985 985
986 def _get_existing_ids(self, post_data):
987 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
986 @classmethod
987 def get_comment_ids(cls, post_data):
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 989
989 990 @LoginRequired()
990 991 @NotAnonymous()
991 992 @HasRepoPermissionAnyDecorator(
992 993 'repository.read', 'repository.write', 'repository.admin')
993 994 @view_config(
994 995 route_name='pullrequest_comments', request_method='POST',
995 996 renderer='string_html', xhr=True)
996 997 def pullrequest_comments(self):
997 998 self.load_default_context()
998 999
999 1000 pull_request = PullRequest.get_or_404(
1000 1001 self.request.matchdict['pull_request_id'])
1001 1002 pull_request_id = pull_request.pull_request_id
1002 1003 version = self.request.GET.get('version')
1003 1004
1004 1005 _render = self.request.get_partial_renderer(
1005 1006 'rhodecode:templates/base/sidebar.mako')
1006 1007 c = _render.get_call_context()
1007 1008
1008 1009 (pull_request_latest,
1009 1010 pull_request_at_ver,
1010 1011 pull_request_display_obj,
1011 1012 at_version) = PullRequestModel().get_pr_version(
1012 1013 pull_request_id, version=version)
1013 1014 versions = pull_request_display_obj.versions()
1014 1015 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1015 1016 c.versions = versions + [latest_ver]
1016 1017
1017 1018 c.at_version = at_version
1018 1019 c.at_version_num = (at_version
1019 1020 if at_version and at_version != PullRequest.LATEST_VER
1020 1021 else None)
1021 1022
1022 1023 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1023 1024 all_comments = c.inline_comments_flat + c.comments
1024 1025
1025 existing_ids = self._get_existing_ids(self.request.POST)
1026 existing_ids = self.get_comment_ids(self.request.POST)
1026 1027 return _render('comments_table', all_comments, len(all_comments),
1027 1028 existing_ids=existing_ids)
1028 1029
1029 1030 @LoginRequired()
1030 1031 @NotAnonymous()
1031 1032 @HasRepoPermissionAnyDecorator(
1032 1033 'repository.read', 'repository.write', 'repository.admin')
1033 1034 @view_config(
1034 1035 route_name='pullrequest_todos', request_method='POST',
1035 1036 renderer='string_html', xhr=True)
1036 1037 def pullrequest_todos(self):
1037 1038 self.load_default_context()
1038 1039
1039 1040 pull_request = PullRequest.get_or_404(
1040 1041 self.request.matchdict['pull_request_id'])
1041 1042 pull_request_id = pull_request.pull_request_id
1042 1043 version = self.request.GET.get('version')
1043 1044
1044 1045 _render = self.request.get_partial_renderer(
1045 1046 'rhodecode:templates/base/sidebar.mako')
1046 1047 c = _render.get_call_context()
1047 1048 (pull_request_latest,
1048 1049 pull_request_at_ver,
1049 1050 pull_request_display_obj,
1050 1051 at_version) = PullRequestModel().get_pr_version(
1051 1052 pull_request_id, version=version)
1052 1053 versions = pull_request_display_obj.versions()
1053 1054 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1054 1055 c.versions = versions + [latest_ver]
1055 1056
1056 1057 c.at_version = at_version
1057 1058 c.at_version_num = (at_version
1058 1059 if at_version and at_version != PullRequest.LATEST_VER
1059 1060 else None)
1060 1061
1061 1062 c.unresolved_comments = CommentsModel() \
1062 1063 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1063 1064 c.resolved_comments = CommentsModel() \
1064 1065 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1065 1066
1066 1067 all_comments = c.unresolved_comments + c.resolved_comments
1067 existing_ids = self._get_existing_ids(self.request.POST)
1068 existing_ids = self.get_comment_ids(self.request.POST)
1068 1069 return _render('comments_table', all_comments, len(c.unresolved_comments),
1069 1070 todo_comments=True, existing_ids=existing_ids)
1070 1071
1071 1072 @LoginRequired()
1072 1073 @NotAnonymous()
1073 1074 @HasRepoPermissionAnyDecorator(
1074 1075 'repository.read', 'repository.write', 'repository.admin')
1075 1076 @CSRFRequired()
1076 1077 @view_config(
1077 1078 route_name='pullrequest_create', request_method='POST',
1078 1079 renderer=None)
1079 1080 def pull_request_create(self):
1080 1081 _ = self.request.translate
1081 1082 self.assure_not_empty_repo()
1082 1083 self.load_default_context()
1083 1084
1084 1085 controls = peppercorn.parse(self.request.POST.items())
1085 1086
1086 1087 try:
1087 1088 form = PullRequestForm(
1088 1089 self.request.translate, self.db_repo.repo_id)()
1089 1090 _form = form.to_python(controls)
1090 1091 except formencode.Invalid as errors:
1091 1092 if errors.error_dict.get('revisions'):
1092 1093 msg = 'Revisions: %s' % errors.error_dict['revisions']
1093 1094 elif errors.error_dict.get('pullrequest_title'):
1094 1095 msg = errors.error_dict.get('pullrequest_title')
1095 1096 else:
1096 1097 msg = _('Error creating pull request: {}').format(errors)
1097 1098 log.exception(msg)
1098 1099 h.flash(msg, 'error')
1099 1100
1100 1101 # would rather just go back to form ...
1101 1102 raise HTTPFound(
1102 1103 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1103 1104
1104 1105 source_repo = _form['source_repo']
1105 1106 source_ref = _form['source_ref']
1106 1107 target_repo = _form['target_repo']
1107 1108 target_ref = _form['target_ref']
1108 1109 commit_ids = _form['revisions'][::-1]
1109 1110 common_ancestor_id = _form['common_ancestor']
1110 1111
1111 1112 # find the ancestor for this pr
1112 1113 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1113 1114 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1114 1115
1115 1116 if not (source_db_repo or target_db_repo):
1116 1117 h.flash(_('source_repo or target repo not found'), category='error')
1117 1118 raise HTTPFound(
1118 1119 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1119 1120
1120 1121 # re-check permissions again here
1121 1122 # source_repo we must have read permissions
1122 1123
1123 1124 source_perm = HasRepoPermissionAny(
1124 1125 'repository.read', 'repository.write', 'repository.admin')(
1125 1126 source_db_repo.repo_name)
1126 1127 if not source_perm:
1127 1128 msg = _('Not Enough permissions to source repo `{}`.'.format(
1128 1129 source_db_repo.repo_name))
1129 1130 h.flash(msg, category='error')
1130 1131 # copy the args back to redirect
1131 1132 org_query = self.request.GET.mixed()
1132 1133 raise HTTPFound(
1133 1134 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1134 1135 _query=org_query))
1135 1136
1136 1137 # target repo we must have read permissions, and also later on
1137 1138 # we want to check branch permissions here
1138 1139 target_perm = HasRepoPermissionAny(
1139 1140 'repository.read', 'repository.write', 'repository.admin')(
1140 1141 target_db_repo.repo_name)
1141 1142 if not target_perm:
1142 1143 msg = _('Not Enough permissions to target repo `{}`.'.format(
1143 1144 target_db_repo.repo_name))
1144 1145 h.flash(msg, category='error')
1145 1146 # copy the args back to redirect
1146 1147 org_query = self.request.GET.mixed()
1147 1148 raise HTTPFound(
1148 1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1149 1150 _query=org_query))
1150 1151
1151 1152 source_scm = source_db_repo.scm_instance()
1152 1153 target_scm = target_db_repo.scm_instance()
1153 1154
1154 1155 source_ref_obj = unicode_to_reference(source_ref)
1155 1156 target_ref_obj = unicode_to_reference(target_ref)
1156 1157
1157 1158 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1158 1159 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1159 1160
1160 1161 ancestor = source_scm.get_common_ancestor(
1161 1162 source_commit.raw_id, target_commit.raw_id, target_scm)
1162 1163
1163 1164 # recalculate target ref based on ancestor
1164 1165 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1165 1166
1166 1167 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1167 1168 PullRequestModel().get_reviewer_functions()
1168 1169
1169 1170 # recalculate reviewers logic, to make sure we can validate this
1170 1171 reviewer_rules = get_default_reviewers_data(
1171 1172 self._rhodecode_db_user,
1172 1173 source_db_repo,
1173 1174 source_ref_obj,
1174 1175 target_db_repo,
1175 1176 target_ref_obj,
1176 1177 include_diff_info=False)
1177 1178
1178 1179 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1179 1180 observers = validate_observers(_form['observer_members'], reviewer_rules)
1180 1181
1181 1182 pullrequest_title = _form['pullrequest_title']
1182 1183 title_source_ref = source_ref_obj.name
1183 1184 if not pullrequest_title:
1184 1185 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1185 1186 source=source_repo,
1186 1187 source_ref=title_source_ref,
1187 1188 target=target_repo
1188 1189 )
1189 1190
1190 1191 description = _form['pullrequest_desc']
1191 1192 description_renderer = _form['description_renderer']
1192 1193
1193 1194 try:
1194 1195 pull_request = PullRequestModel().create(
1195 1196 created_by=self._rhodecode_user.user_id,
1196 1197 source_repo=source_repo,
1197 1198 source_ref=source_ref,
1198 1199 target_repo=target_repo,
1199 1200 target_ref=target_ref,
1200 1201 revisions=commit_ids,
1201 1202 common_ancestor_id=common_ancestor_id,
1202 1203 reviewers=reviewers,
1203 1204 observers=observers,
1204 1205 title=pullrequest_title,
1205 1206 description=description,
1206 1207 description_renderer=description_renderer,
1207 1208 reviewer_data=reviewer_rules,
1208 1209 auth_user=self._rhodecode_user
1209 1210 )
1210 1211 Session().commit()
1211 1212
1212 1213 h.flash(_('Successfully opened new pull request'),
1213 1214 category='success')
1214 1215 except Exception:
1215 1216 msg = _('Error occurred during creation of this pull request.')
1216 1217 log.exception(msg)
1217 1218 h.flash(msg, category='error')
1218 1219
1219 1220 # copy the args back to redirect
1220 1221 org_query = self.request.GET.mixed()
1221 1222 raise HTTPFound(
1222 1223 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1223 1224 _query=org_query))
1224 1225
1225 1226 raise HTTPFound(
1226 1227 h.route_path('pullrequest_show', repo_name=target_repo,
1227 1228 pull_request_id=pull_request.pull_request_id))
1228 1229
1229 1230 @LoginRequired()
1230 1231 @NotAnonymous()
1231 1232 @HasRepoPermissionAnyDecorator(
1232 1233 'repository.read', 'repository.write', 'repository.admin')
1233 1234 @CSRFRequired()
1234 1235 @view_config(
1235 1236 route_name='pullrequest_update', request_method='POST',
1236 1237 renderer='json_ext')
1237 1238 def pull_request_update(self):
1238 1239 pull_request = PullRequest.get_or_404(
1239 1240 self.request.matchdict['pull_request_id'])
1240 1241 _ = self.request.translate
1241 1242
1242 1243 c = self.load_default_context()
1243 1244 redirect_url = None
1244 1245
1245 1246 if pull_request.is_closed():
1246 1247 log.debug('update: forbidden because pull request is closed')
1247 1248 msg = _(u'Cannot update closed pull requests.')
1248 1249 h.flash(msg, category='error')
1249 1250 return {'response': True,
1250 1251 'redirect_url': redirect_url}
1251 1252
1252 1253 is_state_changing = pull_request.is_state_changing()
1253 1254 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1254 1255
1255 1256 # only owner or admin can update it
1256 1257 allowed_to_update = PullRequestModel().check_user_update(
1257 1258 pull_request, self._rhodecode_user)
1258 1259
1259 1260 if allowed_to_update:
1260 1261 controls = peppercorn.parse(self.request.POST.items())
1261 1262 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1262 1263
1263 1264 if 'review_members' in controls:
1264 1265 self._update_reviewers(
1265 1266 c,
1266 1267 pull_request, controls['review_members'],
1267 1268 pull_request.reviewer_data,
1268 1269 PullRequestReviewers.ROLE_REVIEWER)
1269 1270 elif 'observer_members' in controls:
1270 1271 self._update_reviewers(
1271 1272 c,
1272 1273 pull_request, controls['observer_members'],
1273 1274 pull_request.reviewer_data,
1274 1275 PullRequestReviewers.ROLE_OBSERVER)
1275 1276 elif str2bool(self.request.POST.get('update_commits', 'false')):
1276 1277 if is_state_changing:
1277 1278 log.debug('commits update: forbidden because pull request is in state %s',
1278 1279 pull_request.pull_request_state)
1279 1280 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1280 1281 u'Current state is: `{}`').format(
1281 1282 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1282 1283 h.flash(msg, category='error')
1283 1284 return {'response': True,
1284 1285 'redirect_url': redirect_url}
1285 1286
1286 1287 self._update_commits(c, pull_request)
1287 1288 if force_refresh:
1288 1289 redirect_url = h.route_path(
1289 1290 'pullrequest_show', repo_name=self.db_repo_name,
1290 1291 pull_request_id=pull_request.pull_request_id,
1291 1292 _query={"force_refresh": 1})
1292 1293 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1293 1294 self._edit_pull_request(pull_request)
1294 1295 else:
1295 1296 log.error('Unhandled update data.')
1296 1297 raise HTTPBadRequest()
1297 1298
1298 1299 return {'response': True,
1299 1300 'redirect_url': redirect_url}
1300 1301 raise HTTPForbidden()
1301 1302
1302 1303 def _edit_pull_request(self, pull_request):
1303 1304 """
1304 1305 Edit title and description
1305 1306 """
1306 1307 _ = self.request.translate
1307 1308
1308 1309 try:
1309 1310 PullRequestModel().edit(
1310 1311 pull_request,
1311 1312 self.request.POST.get('title'),
1312 1313 self.request.POST.get('description'),
1313 1314 self.request.POST.get('description_renderer'),
1314 1315 self._rhodecode_user)
1315 1316 except ValueError:
1316 1317 msg = _(u'Cannot update closed pull requests.')
1317 1318 h.flash(msg, category='error')
1318 1319 return
1319 1320 else:
1320 1321 Session().commit()
1321 1322
1322 1323 msg = _(u'Pull request title & description updated.')
1323 1324 h.flash(msg, category='success')
1324 1325 return
1325 1326
1326 1327 def _update_commits(self, c, pull_request):
1327 1328 _ = self.request.translate
1328 1329
1329 1330 with pull_request.set_state(PullRequest.STATE_UPDATING):
1330 1331 resp = PullRequestModel().update_commits(
1331 1332 pull_request, self._rhodecode_db_user)
1332 1333
1333 1334 if resp.executed:
1334 1335
1335 1336 if resp.target_changed and resp.source_changed:
1336 1337 changed = 'target and source repositories'
1337 1338 elif resp.target_changed and not resp.source_changed:
1338 1339 changed = 'target repository'
1339 1340 elif not resp.target_changed and resp.source_changed:
1340 1341 changed = 'source repository'
1341 1342 else:
1342 1343 changed = 'nothing'
1343 1344
1344 1345 msg = _(u'Pull request updated to "{source_commit_id}" with '
1345 1346 u'{count_added} added, {count_removed} removed commits. '
1346 1347 u'Source of changes: {change_source}.')
1347 1348 msg = msg.format(
1348 1349 source_commit_id=pull_request.source_ref_parts.commit_id,
1349 1350 count_added=len(resp.changes.added),
1350 1351 count_removed=len(resp.changes.removed),
1351 1352 change_source=changed)
1352 1353 h.flash(msg, category='success')
1353 1354 channelstream.pr_update_channelstream_push(
1354 1355 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1355 1356 else:
1356 1357 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1357 1358 warning_reasons = [
1358 1359 UpdateFailureReason.NO_CHANGE,
1359 1360 UpdateFailureReason.WRONG_REF_TYPE,
1360 1361 ]
1361 1362 category = 'warning' if resp.reason in warning_reasons else 'error'
1362 1363 h.flash(msg, category=category)
1363 1364
1364 1365 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1365 1366 _ = self.request.translate
1366 1367
1367 1368 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1368 1369 PullRequestModel().get_reviewer_functions()
1369 1370
1370 1371 if role == PullRequestReviewers.ROLE_REVIEWER:
1371 1372 try:
1372 1373 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1373 1374 except ValueError as e:
1374 1375 log.error('Reviewers Validation: {}'.format(e))
1375 1376 h.flash(e, category='error')
1376 1377 return
1377 1378
1378 1379 old_calculated_status = pull_request.calculated_review_status()
1379 1380 PullRequestModel().update_reviewers(
1380 1381 pull_request, reviewers, self._rhodecode_db_user)
1381 1382
1382 1383 Session().commit()
1383 1384
1384 1385 msg = _('Pull request reviewers updated.')
1385 1386 h.flash(msg, category='success')
1386 1387 channelstream.pr_update_channelstream_push(
1387 1388 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1388 1389
1389 1390 # trigger status changed if change in reviewers changes the status
1390 1391 calculated_status = pull_request.calculated_review_status()
1391 1392 if old_calculated_status != calculated_status:
1392 1393 PullRequestModel().trigger_pull_request_hook(
1393 1394 pull_request, self._rhodecode_user, 'review_status_change',
1394 1395 data={'status': calculated_status})
1395 1396
1396 1397 elif role == PullRequestReviewers.ROLE_OBSERVER:
1397 1398 try:
1398 1399 observers = validate_observers(review_members, reviewer_rules)
1399 1400 except ValueError as e:
1400 1401 log.error('Observers Validation: {}'.format(e))
1401 1402 h.flash(e, category='error')
1402 1403 return
1403 1404
1404 1405 PullRequestModel().update_observers(
1405 1406 pull_request, observers, self._rhodecode_db_user)
1406 1407
1407 1408 Session().commit()
1408 1409 msg = _('Pull request observers updated.')
1409 1410 h.flash(msg, category='success')
1410 1411 channelstream.pr_update_channelstream_push(
1411 1412 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1412 1413
1413 1414 @LoginRequired()
1414 1415 @NotAnonymous()
1415 1416 @HasRepoPermissionAnyDecorator(
1416 1417 'repository.read', 'repository.write', 'repository.admin')
1417 1418 @CSRFRequired()
1418 1419 @view_config(
1419 1420 route_name='pullrequest_merge', request_method='POST',
1420 1421 renderer='json_ext')
1421 1422 def pull_request_merge(self):
1422 1423 """
1423 1424 Merge will perform a server-side merge of the specified
1424 1425 pull request, if the pull request is approved and mergeable.
1425 1426 After successful merging, the pull request is automatically
1426 1427 closed, with a relevant comment.
1427 1428 """
1428 1429 pull_request = PullRequest.get_or_404(
1429 1430 self.request.matchdict['pull_request_id'])
1430 1431 _ = self.request.translate
1431 1432
1432 1433 if pull_request.is_state_changing():
1433 1434 log.debug('show: forbidden because pull request is in state %s',
1434 1435 pull_request.pull_request_state)
1435 1436 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1436 1437 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1437 1438 pull_request.pull_request_state)
1438 1439 h.flash(msg, category='error')
1439 1440 raise HTTPFound(
1440 1441 h.route_path('pullrequest_show',
1441 1442 repo_name=pull_request.target_repo.repo_name,
1442 1443 pull_request_id=pull_request.pull_request_id))
1443 1444
1444 1445 self.load_default_context()
1445 1446
1446 1447 with pull_request.set_state(PullRequest.STATE_UPDATING):
1447 1448 check = MergeCheck.validate(
1448 1449 pull_request, auth_user=self._rhodecode_user,
1449 1450 translator=self.request.translate)
1450 1451 merge_possible = not check.failed
1451 1452
1452 1453 for err_type, error_msg in check.errors:
1453 1454 h.flash(error_msg, category=err_type)
1454 1455
1455 1456 if merge_possible:
1456 1457 log.debug("Pre-conditions checked, trying to merge.")
1457 1458 extras = vcs_operation_context(
1458 1459 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1459 1460 username=self._rhodecode_db_user.username, action='push',
1460 1461 scm=pull_request.target_repo.repo_type)
1461 1462 with pull_request.set_state(PullRequest.STATE_UPDATING):
1462 1463 self._merge_pull_request(
1463 1464 pull_request, self._rhodecode_db_user, extras)
1464 1465 else:
1465 1466 log.debug("Pre-conditions failed, NOT merging.")
1466 1467
1467 1468 raise HTTPFound(
1468 1469 h.route_path('pullrequest_show',
1469 1470 repo_name=pull_request.target_repo.repo_name,
1470 1471 pull_request_id=pull_request.pull_request_id))
1471 1472
1472 1473 def _merge_pull_request(self, pull_request, user, extras):
1473 1474 _ = self.request.translate
1474 1475 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1475 1476
1476 1477 if merge_resp.executed:
1477 1478 log.debug("The merge was successful, closing the pull request.")
1478 1479 PullRequestModel().close_pull_request(
1479 1480 pull_request.pull_request_id, user)
1480 1481 Session().commit()
1481 1482 msg = _('Pull request was successfully merged and closed.')
1482 1483 h.flash(msg, category='success')
1483 1484 else:
1484 1485 log.debug(
1485 1486 "The merge was not successful. Merge response: %s", merge_resp)
1486 1487 msg = merge_resp.merge_status_message
1487 1488 h.flash(msg, category='error')
1488 1489
1489 1490 @LoginRequired()
1490 1491 @NotAnonymous()
1491 1492 @HasRepoPermissionAnyDecorator(
1492 1493 'repository.read', 'repository.write', 'repository.admin')
1493 1494 @CSRFRequired()
1494 1495 @view_config(
1495 1496 route_name='pullrequest_delete', request_method='POST',
1496 1497 renderer='json_ext')
1497 1498 def pull_request_delete(self):
1498 1499 _ = self.request.translate
1499 1500
1500 1501 pull_request = PullRequest.get_or_404(
1501 1502 self.request.matchdict['pull_request_id'])
1502 1503 self.load_default_context()
1503 1504
1504 1505 pr_closed = pull_request.is_closed()
1505 1506 allowed_to_delete = PullRequestModel().check_user_delete(
1506 1507 pull_request, self._rhodecode_user) and not pr_closed
1507 1508
1508 1509 # only owner can delete it !
1509 1510 if allowed_to_delete:
1510 1511 PullRequestModel().delete(pull_request, self._rhodecode_user)
1511 1512 Session().commit()
1512 1513 h.flash(_('Successfully deleted pull request'),
1513 1514 category='success')
1514 1515 raise HTTPFound(h.route_path('pullrequest_show_all',
1515 1516 repo_name=self.db_repo_name))
1516 1517
1517 1518 log.warning('user %s tried to delete pull request without access',
1518 1519 self._rhodecode_user)
1519 1520 raise HTTPNotFound()
1520 1521
1522 def _pull_request_comments_create(self, pull_request, comments):
1523 _ = self.request.translate
1524 data = {}
1525 pull_request_id = pull_request.pull_request_id
1526 if not comments:
1527 return
1528
1529 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1530
1531 for entry in comments:
1532 c = self.load_default_context()
1533 comment_type = entry['comment_type']
1534 text = entry['text']
1535 status = entry['status']
1536 is_draft = str2bool(entry['is_draft'])
1537 resolves_comment_id = entry['resolves_comment_id']
1538 close_pull_request = entry['close_pull_request']
1539 f_path = entry['f_path']
1540 line_no = entry['line']
1541 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1542
1543 # the logic here should work like following, if we submit close
1544 # pr comment, use `close_pull_request_with_comment` function
1545 # else handle regular comment logic
1546
1547 if close_pull_request:
1548 # only owner or admin or person with write permissions
1549 allowed_to_close = PullRequestModel().check_user_update(
1550 pull_request, self._rhodecode_user)
1551 if not allowed_to_close:
1552 log.debug('comment: forbidden because not allowed to close '
1553 'pull request %s', pull_request_id)
1554 raise HTTPForbidden()
1555
1556 # This also triggers `review_status_change`
1557 comment, status = PullRequestModel().close_pull_request_with_comment(
1558 pull_request, self._rhodecode_user, self.db_repo, message=text,
1559 auth_user=self._rhodecode_user)
1560 Session().flush()
1561 is_inline = comment.is_inline
1562
1563 PullRequestModel().trigger_pull_request_hook(
1564 pull_request, self._rhodecode_user, 'comment',
1565 data={'comment': comment})
1566
1567 else:
1568 # regular comment case, could be inline, or one with status.
1569 # for that one we check also permissions
1570 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1571 allowed_to_change_status = PullRequestModel().check_user_change_status(
1572 pull_request, self._rhodecode_user) and not is_draft
1573
1574 if status and allowed_to_change_status:
1575 message = (_('Status change %(transition_icon)s %(status)s')
1576 % {'transition_icon': '>',
1577 'status': ChangesetStatus.get_status_lbl(status)})
1578 text = text or message
1579
1580 comment = CommentsModel().create(
1581 text=text,
1582 repo=self.db_repo.repo_id,
1583 user=self._rhodecode_user.user_id,
1584 pull_request=pull_request,
1585 f_path=f_path,
1586 line_no=line_no,
1587 status_change=(ChangesetStatus.get_status_lbl(status)
1588 if status and allowed_to_change_status else None),
1589 status_change_type=(status
1590 if status and allowed_to_change_status else None),
1591 comment_type=comment_type,
1592 is_draft=is_draft,
1593 resolves_comment_id=resolves_comment_id,
1594 auth_user=self._rhodecode_user,
1595 send_email=not is_draft, # skip notification for draft comments
1596 )
1597 is_inline = comment.is_inline
1598
1599 if allowed_to_change_status:
1600 # calculate old status before we change it
1601 old_calculated_status = pull_request.calculated_review_status()
1602
1603 # get status if set !
1604 if status:
1605 ChangesetStatusModel().set_status(
1606 self.db_repo.repo_id,
1607 status,
1608 self._rhodecode_user.user_id,
1609 comment,
1610 pull_request=pull_request
1611 )
1612
1613 Session().flush()
1614 # this is somehow required to get access to some relationship
1615 # loaded on comment
1616 Session().refresh(comment)
1617
1618 PullRequestModel().trigger_pull_request_hook(
1619 pull_request, self._rhodecode_user, 'comment',
1620 data={'comment': comment})
1621
1622 # we now calculate the status of pull request, and based on that
1623 # calculation we set the commits status
1624 calculated_status = pull_request.calculated_review_status()
1625 if old_calculated_status != calculated_status:
1626 PullRequestModel().trigger_pull_request_hook(
1627 pull_request, self._rhodecode_user, 'review_status_change',
1628 data={'status': calculated_status})
1629
1630 comment_id = comment.comment_id
1631 data[comment_id] = {
1632 'target_id': target_elem_id
1633 }
1634 Session().flush()
1635
1636 c.co = comment
1637 c.at_version_num = None
1638 c.is_new = True
1639 rendered_comment = render(
1640 'rhodecode:templates/changeset/changeset_comment_block.mako',
1641 self._get_template_context(c), self.request)
1642
1643 data[comment_id].update(comment.get_dict())
1644 data[comment_id].update({'rendered_text': rendered_comment})
1645
1646 Session().commit()
1647
1648 # skip channelstream for draft comments
1649 if all_drafts:
1650 comment_broadcast_channel = channelstream.comment_channel(
1651 self.db_repo_name, pull_request_obj=pull_request)
1652
1653 comment_data = data
1654 comment_type = 'inline' if is_inline else 'general'
1655 if len(data) == 1:
1656 msg = _('posted {} new {} comment').format(len(data), comment_type)
1657 else:
1658 msg = _('posted {} new {} comments').format(len(data), comment_type)
1659
1660 channelstream.comment_channelstream_push(
1661 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1662 comment_data=comment_data)
1663
1664 return data
1665
1521 1666 @LoginRequired()
1522 1667 @NotAnonymous()
1523 1668 @HasRepoPermissionAnyDecorator(
1524 1669 'repository.read', 'repository.write', 'repository.admin')
1525 1670 @CSRFRequired()
1526 1671 @view_config(
1527 1672 route_name='pullrequest_comment_create', request_method='POST',
1528 1673 renderer='json_ext')
1529 1674 def pull_request_comment_create(self):
1530 1675 _ = self.request.translate
1531 1676
1532 pull_request = PullRequest.get_or_404(
1533 self.request.matchdict['pull_request_id'])
1534 pull_request_id = pull_request.pull_request_id
1677 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1535 1678
1536 1679 if pull_request.is_closed():
1537 1680 log.debug('comment: forbidden because pull request is closed')
1538 1681 raise HTTPForbidden()
1539 1682
1540 1683 allowed_to_comment = PullRequestModel().check_user_comment(
1541 1684 pull_request, self._rhodecode_user)
1542 1685 if not allowed_to_comment:
1543 1686 log.debug('comment: forbidden because pull request is from forbidden repo')
1544 1687 raise HTTPForbidden()
1545 1688
1546 c = self.load_default_context()
1547
1548 status = self.request.POST.get('changeset_status', None)
1549 text = self.request.POST.get('text')
1550 comment_type = self.request.POST.get('comment_type')
1551 is_draft = str2bool(self.request.POST.get('draft'))
1552 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1553 close_pull_request = self.request.POST.get('close_pull_request')
1554
1555 # the logic here should work like following, if we submit close
1556 # pr comment, use `close_pull_request_with_comment` function
1557 # else handle regular comment logic
1558
1559 if close_pull_request:
1560 # only owner or admin or person with write permissions
1561 allowed_to_close = PullRequestModel().check_user_update(
1562 pull_request, self._rhodecode_user)
1563 if not allowed_to_close:
1564 log.debug('comment: forbidden because not allowed to close '
1565 'pull request %s', pull_request_id)
1566 raise HTTPForbidden()
1567
1568 # This also triggers `review_status_change`
1569 comment, status = PullRequestModel().close_pull_request_with_comment(
1570 pull_request, self._rhodecode_user, self.db_repo, message=text,
1571 auth_user=self._rhodecode_user)
1572 Session().flush()
1573 is_inline = comment.is_inline
1574
1575 PullRequestModel().trigger_pull_request_hook(
1576 pull_request, self._rhodecode_user, 'comment',
1577 data={'comment': comment})
1578
1579 else:
1580 # regular comment case, could be inline, or one with status.
1581 # for that one we check also permissions
1582 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1583 allowed_to_change_status = PullRequestModel().check_user_change_status(
1584 pull_request, self._rhodecode_user) and not is_draft
1585
1586 if status and allowed_to_change_status:
1587 message = (_('Status change %(transition_icon)s %(status)s')
1588 % {'transition_icon': '>',
1589 'status': ChangesetStatus.get_status_lbl(status)})
1590 text = text or message
1591
1592 comment = CommentsModel().create(
1593 text=text,
1594 repo=self.db_repo.repo_id,
1595 user=self._rhodecode_user.user_id,
1596 pull_request=pull_request,
1597 f_path=self.request.POST.get('f_path'),
1598 line_no=self.request.POST.get('line'),
1599 status_change=(ChangesetStatus.get_status_lbl(status)
1600 if status and allowed_to_change_status else None),
1601 status_change_type=(status
1602 if status and allowed_to_change_status else None),
1603 comment_type=comment_type,
1604 is_draft=is_draft,
1605 resolves_comment_id=resolves_comment_id,
1606 auth_user=self._rhodecode_user,
1607 send_email=not is_draft, # skip notification for draft comments
1608 )
1609 is_inline = comment.is_inline
1610
1611 if allowed_to_change_status:
1612 # calculate old status before we change it
1613 old_calculated_status = pull_request.calculated_review_status()
1614
1615 # get status if set !
1616 if status:
1617 ChangesetStatusModel().set_status(
1618 self.db_repo.repo_id,
1619 status,
1620 self._rhodecode_user.user_id,
1621 comment,
1622 pull_request=pull_request
1623 )
1624
1625 Session().flush()
1626 # this is somehow required to get access to some relationship
1627 # loaded on comment
1628 Session().refresh(comment)
1629
1630 PullRequestModel().trigger_pull_request_hook(
1631 pull_request, self._rhodecode_user, 'comment',
1632 data={'comment': comment})
1633
1634 # we now calculate the status of pull request, and based on that
1635 # calculation we set the commits status
1636 calculated_status = pull_request.calculated_review_status()
1637 if old_calculated_status != calculated_status:
1638 PullRequestModel().trigger_pull_request_hook(
1639 pull_request, self._rhodecode_user, 'review_status_change',
1640 data={'status': calculated_status})
1641
1642 Session().commit()
1643
1644 data = {
1645 'target_id': h.safeid(h.safe_unicode(
1646 self.request.POST.get('f_path'))),
1689 comment_data = {
1690 'comment_type': self.request.POST.get('comment_type'),
1691 'text': self.request.POST.get('text'),
1692 'status': self.request.POST.get('changeset_status', None),
1693 'is_draft': self.request.POST.get('draft'),
1694 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 'f_path': self.request.POST.get('f_path'),
1697 'line': self.request.POST.get('line'),
1647 1698 }
1648
1649 if comment:
1650 c.co = comment
1651 c.at_version_num = None
1652 rendered_comment = render(
1653 'rhodecode:templates/changeset/changeset_comment_block.mako',
1654 self._get_template_context(c), self.request)
1655
1656 data.update(comment.get_dict())
1657 data.update({'rendered_text': rendered_comment})
1658
1659 # skip channelstream for draft comments
1660 if not is_draft:
1661 comment_broadcast_channel = channelstream.comment_channel(
1662 self.db_repo_name, pull_request_obj=pull_request)
1663
1664 comment_data = data
1665 comment_type = 'inline' if is_inline else 'general'
1666 channelstream.comment_channelstream_push(
1667 self.request, comment_broadcast_channel, self._rhodecode_user,
1668 _('posted a new {} comment').format(comment_type),
1669 comment_data=comment_data)
1699 data = self._pull_request_comments_create(pull_request, [comment_data])
1670 1700
1671 1701 return data
1672 1702
1673 1703 @LoginRequired()
1674 1704 @NotAnonymous()
1675 1705 @HasRepoPermissionAnyDecorator(
1676 1706 'repository.read', 'repository.write', 'repository.admin')
1677 1707 @CSRFRequired()
1678 1708 @view_config(
1679 1709 route_name='pullrequest_comment_delete', request_method='POST',
1680 1710 renderer='json_ext')
1681 1711 def pull_request_comment_delete(self):
1682 1712 pull_request = PullRequest.get_or_404(
1683 1713 self.request.matchdict['pull_request_id'])
1684 1714
1685 1715 comment = ChangesetComment.get_or_404(
1686 1716 self.request.matchdict['comment_id'])
1687 1717 comment_id = comment.comment_id
1688 1718
1689 1719 if comment.immutable:
1690 1720 # don't allow deleting comments that are immutable
1691 1721 raise HTTPForbidden()
1692 1722
1693 1723 if pull_request.is_closed():
1694 1724 log.debug('comment: forbidden because pull request is closed')
1695 1725 raise HTTPForbidden()
1696 1726
1697 1727 if not comment:
1698 1728 log.debug('Comment with id:%s not found, skipping', comment_id)
1699 1729 # comment already deleted in another call probably
1700 1730 return True
1701 1731
1702 1732 if comment.pull_request.is_closed():
1703 1733 # don't allow deleting comments on closed pull request
1704 1734 raise HTTPForbidden()
1705 1735
1706 1736 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1707 1737 super_admin = h.HasPermissionAny('hg.admin')()
1708 1738 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1709 1739 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1710 1740 comment_repo_admin = is_repo_admin and is_repo_comment
1711 1741
1712 1742 if super_admin or comment_owner or comment_repo_admin:
1713 1743 old_calculated_status = comment.pull_request.calculated_review_status()
1714 1744 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1715 1745 Session().commit()
1716 1746 calculated_status = comment.pull_request.calculated_review_status()
1717 1747 if old_calculated_status != calculated_status:
1718 1748 PullRequestModel().trigger_pull_request_hook(
1719 1749 comment.pull_request, self._rhodecode_user, 'review_status_change',
1720 1750 data={'status': calculated_status})
1721 1751 return True
1722 1752 else:
1723 1753 log.warning('No permissions for user %s to delete comment_id: %s',
1724 1754 self._rhodecode_db_user, comment_id)
1725 1755 raise HTTPNotFound()
1726 1756
1727 1757 @LoginRequired()
1728 1758 @NotAnonymous()
1729 1759 @HasRepoPermissionAnyDecorator(
1730 1760 'repository.read', 'repository.write', 'repository.admin')
1731 1761 @CSRFRequired()
1732 1762 @view_config(
1733 1763 route_name='pullrequest_comment_edit', request_method='POST',
1734 1764 renderer='json_ext')
1735 1765 def pull_request_comment_edit(self):
1736 1766 self.load_default_context()
1737 1767
1738 1768 pull_request = PullRequest.get_or_404(
1739 1769 self.request.matchdict['pull_request_id']
1740 1770 )
1741 1771 comment = ChangesetComment.get_or_404(
1742 1772 self.request.matchdict['comment_id']
1743 1773 )
1744 1774 comment_id = comment.comment_id
1745 1775
1746 1776 if comment.immutable:
1747 1777 # don't allow deleting comments that are immutable
1748 1778 raise HTTPForbidden()
1749 1779
1750 1780 if pull_request.is_closed():
1751 1781 log.debug('comment: forbidden because pull request is closed')
1752 1782 raise HTTPForbidden()
1753 1783
1754 1784 if not comment:
1755 1785 log.debug('Comment with id:%s not found, skipping', comment_id)
1756 1786 # comment already deleted in another call probably
1757 1787 return True
1758 1788
1759 1789 if comment.pull_request.is_closed():
1760 1790 # don't allow deleting comments on closed pull request
1761 1791 raise HTTPForbidden()
1762 1792
1763 1793 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1764 1794 super_admin = h.HasPermissionAny('hg.admin')()
1765 1795 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1766 1796 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1767 1797 comment_repo_admin = is_repo_admin and is_repo_comment
1768 1798
1769 1799 if super_admin or comment_owner or comment_repo_admin:
1770 1800 text = self.request.POST.get('text')
1771 1801 version = self.request.POST.get('version')
1772 1802 if text == comment.text:
1773 1803 log.warning(
1774 1804 'Comment(PR): '
1775 1805 'Trying to create new version '
1776 1806 'with the same comment body {}'.format(
1777 1807 comment_id,
1778 1808 )
1779 1809 )
1780 1810 raise HTTPNotFound()
1781 1811
1782 1812 if version.isdigit():
1783 1813 version = int(version)
1784 1814 else:
1785 1815 log.warning(
1786 1816 'Comment(PR): Wrong version type {} {} '
1787 1817 'for comment {}'.format(
1788 1818 version,
1789 1819 type(version),
1790 1820 comment_id,
1791 1821 )
1792 1822 )
1793 1823 raise HTTPNotFound()
1794 1824
1795 1825 try:
1796 1826 comment_history = CommentsModel().edit(
1797 1827 comment_id=comment_id,
1798 1828 text=text,
1799 1829 auth_user=self._rhodecode_user,
1800 1830 version=version,
1801 1831 )
1802 1832 except CommentVersionMismatch:
1803 1833 raise HTTPConflict()
1804 1834
1805 1835 if not comment_history:
1806 1836 raise HTTPNotFound()
1807 1837
1808 1838 Session().commit()
1809 1839
1810 1840 PullRequestModel().trigger_pull_request_hook(
1811 1841 pull_request, self._rhodecode_user, 'comment_edit',
1812 1842 data={'comment': comment})
1813 1843
1814 1844 return {
1815 1845 'comment_history_id': comment_history.comment_history_id,
1816 1846 'comment_id': comment.comment_id,
1817 1847 'comment_version': comment_history.version,
1818 1848 'comment_author_username': comment_history.author.username,
1819 1849 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1820 1850 'comment_created_on': h.age_component(comment_history.created_on,
1821 1851 time_is_local=True),
1822 1852 }
1823 1853 else:
1824 1854 log.warning('No permissions for user %s to edit comment_id: %s',
1825 1855 self._rhodecode_db_user, comment_id)
1826 1856 raise HTTPNotFound()
@@ -1,371 +1,370 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import hashlib
23 23 import itsdangerous
24 24 import logging
25 25 import requests
26 26 import datetime
27 27
28 28 from dogpile.core import ReadWriteMutex
29 29 from pyramid.threadlocal import get_current_registry
30 30
31 31 import rhodecode.lib.helpers as h
32 32 from rhodecode.lib.auth import HasRepoPermissionAny
33 33 from rhodecode.lib.ext_json import json
34 34 from rhodecode.model.db import User
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38 LOCK = ReadWriteMutex()
39 39
40 40 USER_STATE_PUBLIC_KEYS = [
41 41 'id', 'username', 'first_name', 'last_name',
42 42 'icon_link', 'display_name', 'display_link']
43 43
44 44
45 45 class ChannelstreamException(Exception):
46 46 pass
47 47
48 48
49 49 class ChannelstreamConnectionException(ChannelstreamException):
50 50 pass
51 51
52 52
53 53 class ChannelstreamPermissionException(ChannelstreamException):
54 54 pass
55 55
56 56
57 57 def get_channelstream_server_url(config, endpoint):
58 58 return 'http://{}{}'.format(config['server'], endpoint)
59 59
60 60
61 61 def channelstream_request(config, payload, endpoint, raise_exc=True):
62 62 signer = itsdangerous.TimestampSigner(config['secret'])
63 63 sig_for_server = signer.sign(endpoint)
64 64 secret_headers = {'x-channelstream-secret': sig_for_server,
65 65 'x-channelstream-endpoint': endpoint,
66 66 'Content-Type': 'application/json'}
67 67 req_url = get_channelstream_server_url(config, endpoint)
68 68
69 69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
70 70 response = None
71 71 try:
72 72 response = requests.post(req_url, data=json.dumps(payload),
73 73 headers=secret_headers).json()
74 74 except requests.ConnectionError:
75 75 log.exception('ConnectionError occurred for endpoint %s', req_url)
76 76 if raise_exc:
77 77 raise ChannelstreamConnectionException(req_url)
78 78 except Exception:
79 79 log.exception('Exception related to Channelstream happened')
80 80 if raise_exc:
81 81 raise ChannelstreamConnectionException()
82 82 log.debug('Got channelstream response: %s', response)
83 83 return response
84 84
85 85
86 86 def get_user_data(user_id):
87 87 user = User.get(user_id)
88 88 return {
89 89 'id': user.user_id,
90 90 'username': user.username,
91 91 'first_name': user.first_name,
92 92 'last_name': user.last_name,
93 93 'icon_link': h.gravatar_url(user.email, 60),
94 94 'display_name': h.person(user, 'username_or_name_or_email'),
95 95 'display_link': h.link_to_user(user),
96 96 'notifications': user.user_data.get('notification_status', True)
97 97 }
98 98
99 99
100 100 def broadcast_validator(channel_name):
101 101 """ checks if user can access the broadcast channel """
102 102 if channel_name == 'broadcast':
103 103 return True
104 104
105 105
106 106 def repo_validator(channel_name):
107 107 """ checks if user can access the broadcast channel """
108 108 channel_prefix = '/repo$'
109 109 if channel_name.startswith(channel_prefix):
110 110 elements = channel_name[len(channel_prefix):].split('$')
111 111 repo_name = elements[0]
112 112 can_access = HasRepoPermissionAny(
113 113 'repository.read',
114 114 'repository.write',
115 115 'repository.admin')(repo_name)
116 116 log.debug(
117 117 'permission check for %s channel resulted in %s',
118 118 repo_name, can_access)
119 119 if can_access:
120 120 return True
121 121 return False
122 122
123 123
124 124 def check_channel_permissions(channels, plugin_validators, should_raise=True):
125 125 valid_channels = []
126 126
127 127 validators = [broadcast_validator, repo_validator]
128 128 if plugin_validators:
129 129 validators.extend(plugin_validators)
130 130 for channel_name in channels:
131 131 is_valid = False
132 132 for validator in validators:
133 133 if validator(channel_name):
134 134 is_valid = True
135 135 break
136 136 if is_valid:
137 137 valid_channels.append(channel_name)
138 138 else:
139 139 if should_raise:
140 140 raise ChannelstreamPermissionException()
141 141 return valid_channels
142 142
143 143
144 144 def get_channels_info(self, channels):
145 145 payload = {'channels': channels}
146 146 # gather persistence info
147 147 return channelstream_request(self._config(), payload, '/info')
148 148
149 149
150 150 def parse_channels_info(info_result, include_channel_info=None):
151 151 """
152 152 Returns data that contains only secure information that can be
153 153 presented to clients
154 154 """
155 155 include_channel_info = include_channel_info or []
156 156
157 157 user_state_dict = {}
158 158 for userinfo in info_result['users']:
159 159 user_state_dict[userinfo['user']] = {
160 160 k: v for k, v in userinfo['state'].items()
161 161 if k in USER_STATE_PUBLIC_KEYS
162 162 }
163 163
164 164 channels_info = {}
165 165
166 166 for c_name, c_info in info_result['channels'].items():
167 167 if c_name not in include_channel_info:
168 168 continue
169 169 connected_list = []
170 170 for username in c_info['users']:
171 171 connected_list.append({
172 172 'user': username,
173 173 'state': user_state_dict[username]
174 174 })
175 175 channels_info[c_name] = {'users': connected_list,
176 176 'history': c_info['history']}
177 177
178 178 return channels_info
179 179
180 180
181 181 def log_filepath(history_location, channel_name):
182 182 hasher = hashlib.sha256()
183 183 hasher.update(channel_name.encode('utf8'))
184 184 filename = '{}.log'.format(hasher.hexdigest())
185 185 filepath = os.path.join(history_location, filename)
186 186 return filepath
187 187
188 188
189 189 def read_history(history_location, channel_name):
190 190 filepath = log_filepath(history_location, channel_name)
191 191 if not os.path.exists(filepath):
192 192 return []
193 193 history_lines_limit = -100
194 194 history = []
195 195 with open(filepath, 'rb') as f:
196 196 for line in f.readlines()[history_lines_limit:]:
197 197 try:
198 198 history.append(json.loads(line))
199 199 except Exception:
200 200 log.exception('Failed to load history')
201 201 return history
202 202
203 203
204 204 def update_history_from_logs(config, channels, payload):
205 205 history_location = config.get('history.location')
206 206 for channel in channels:
207 207 history = read_history(history_location, channel)
208 208 payload['channels_info'][channel]['history'] = history
209 209
210 210
211 211 def write_history(config, message):
212 212 """ writes a messge to a base64encoded filename """
213 213 history_location = config.get('history.location')
214 214 if not os.path.exists(history_location):
215 215 return
216 216 try:
217 217 LOCK.acquire_write_lock()
218 218 filepath = log_filepath(history_location, message['channel'])
219 219 with open(filepath, 'ab') as f:
220 220 json.dump(message, f)
221 221 f.write('\n')
222 222 finally:
223 223 LOCK.release_write_lock()
224 224
225 225
226 226 def get_connection_validators(registry):
227 227 validators = []
228 228 for k, config in registry.rhodecode_plugins.items():
229 229 validator = config.get('channelstream', {}).get('connect_validator')
230 230 if validator:
231 231 validators.append(validator)
232 232 return validators
233 233
234 234
235 235 def get_channelstream_config(registry=None):
236 236 if not registry:
237 237 registry = get_current_registry()
238 238
239 239 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
240 240 channelstream_config = rhodecode_plugins.get('channelstream', {})
241 241 return channelstream_config
242 242
243 243
244 244 def post_message(channel, message, username, registry=None):
245 245 channelstream_config = get_channelstream_config(registry)
246 246 if not channelstream_config.get('enabled'):
247 247 return
248 248
249 249 message_obj = message
250 250 if isinstance(message, basestring):
251 251 message_obj = {
252 252 'message': message,
253 253 'level': 'success',
254 254 'topic': '/notifications'
255 255 }
256 256
257 257 log.debug('Channelstream: sending notification to channel %s', channel)
258 258 payload = {
259 259 'type': 'message',
260 260 'timestamp': datetime.datetime.utcnow(),
261 261 'user': 'system',
262 262 'exclude_users': [username],
263 263 'channel': channel,
264 264 'message': message_obj
265 265 }
266 266
267 267 try:
268 268 return channelstream_request(
269 269 channelstream_config, [payload], '/message',
270 270 raise_exc=False)
271 271 except ChannelstreamException:
272 272 log.exception('Failed to send channelstream data')
273 273 raise
274 274
275 275
276 276 def _reload_link(label):
277 277 return (
278 278 '<a onclick="window.location.reload()">'
279 279 '<strong>{}</strong>'
280 280 '</a>'.format(label)
281 281 )
282 282
283 283
284 284 def pr_channel(pull_request):
285 285 repo_name = pull_request.target_repo.repo_name
286 286 pull_request_id = pull_request.pull_request_id
287 287 channel = '/repo${}$/pr/{}'.format(repo_name, pull_request_id)
288 288 log.debug('Getting pull-request channelstream broadcast channel: %s', channel)
289 289 return channel
290 290
291 291
292 292 def comment_channel(repo_name, commit_obj=None, pull_request_obj=None):
293 293 channel = None
294 294 if commit_obj:
295 295 channel = u'/repo${}$/commit/{}'.format(
296 296 repo_name, commit_obj.raw_id
297 297 )
298 298 elif pull_request_obj:
299 299 channel = u'/repo${}$/pr/{}'.format(
300 300 repo_name, pull_request_obj.pull_request_id
301 301 )
302 302 log.debug('Getting comment channelstream broadcast channel: %s', channel)
303 303
304 304 return channel
305 305
306 306
307 307 def pr_update_channelstream_push(request, pr_broadcast_channel, user, msg, **kwargs):
308 308 """
309 309 Channel push on pull request update
310 310 """
311 311 if not pr_broadcast_channel:
312 312 return
313 313
314 314 _ = request.translate
315 315
316 316 message = '{} {}'.format(
317 317 msg,
318 318 _reload_link(_(' Reload page to load changes')))
319 319
320 320 message_obj = {
321 321 'message': message,
322 322 'level': 'success',
323 323 'topic': '/notifications'
324 324 }
325 325
326 326 post_message(
327 327 pr_broadcast_channel, message_obj, user.username,
328 328 registry=request.registry)
329 329
330 330
331 331 def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **kwargs):
332 332 """
333 333 Channelstream push on comment action, on commit, or pull-request
334 334 """
335 335 if not comment_broadcast_channel:
336 336 return
337 337
338 338 _ = request.translate
339 339
340 340 comment_data = kwargs.pop('comment_data', {})
341 341 user_data = kwargs.pop('user_data', {})
342 comment_id = comment_data.get('comment_id')
342 comment_id = comment_data.keys()[0] if comment_data else ''
343 343
344 message = '<strong>{}</strong> {} #{}, {}'.format(
344 message = '<strong>{}</strong> {} #{}'.format(
345 345 user.username,
346 346 msg,
347 347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 348 )
350 349
351 350 message_obj = {
352 351 'message': message,
353 352 'level': 'success',
354 353 'topic': '/notifications'
355 354 }
356 355
357 356 post_message(
358 357 comment_broadcast_channel, message_obj, user.username,
359 358 registry=request.registry)
360 359
361 360 message_obj = {
362 361 'message': None,
363 362 'user': user.username,
364 363 'comment_id': comment_id,
365 364 'comment_data': comment_data,
366 365 'user_data': user_data,
367 366 'topic': '/comment'
368 367 }
369 368 post_message(
370 369 comment_broadcast_channel, message_obj, user.username,
371 370 registry=request.registry)
@@ -1,1272 +1,1272 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of diffing helpers, previously part of vcs
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import bz2
29 29 import gzip
30 30 import time
31 31
32 32 import collections
33 33 import difflib
34 34 import logging
35 35 import cPickle as pickle
36 36 from itertools import tee, imap
37 37
38 38 from rhodecode.lib.vcs.exceptions import VCSError
39 39 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 # define max context, a file with more than this numbers of lines is unusable
45 45 # in browser anyway
46 46 MAX_CONTEXT = 20 * 1024
47 47 DEFAULT_CONTEXT = 3
48 48
49 49
50 50 def get_diff_context(request):
51 51 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
52 52
53 53
54 54 def get_diff_whitespace_flag(request):
55 55 return request.GET.get('ignorews', '') == '1'
56 56
57 57
58 58 class OPS(object):
59 59 ADD = 'A'
60 60 MOD = 'M'
61 61 DEL = 'D'
62 62
63 63
64 64 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
65 65 """
66 66 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
67 67
68 68 :param ignore_whitespace: ignore whitespaces in diff
69 69 """
70 70 # make sure we pass in default context
71 71 context = context or 3
72 72 # protect against IntOverflow when passing HUGE context
73 73 if context > MAX_CONTEXT:
74 74 context = MAX_CONTEXT
75 75
76 76 submodules = filter(lambda o: isinstance(o, SubModuleNode),
77 77 [filenode_new, filenode_old])
78 78 if submodules:
79 79 return ''
80 80
81 81 for filenode in (filenode_old, filenode_new):
82 82 if not isinstance(filenode, FileNode):
83 83 raise VCSError(
84 84 "Given object should be FileNode object, not %s"
85 85 % filenode.__class__)
86 86
87 87 repo = filenode_new.commit.repository
88 88 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
89 89 new_commit = filenode_new.commit
90 90
91 91 vcs_gitdiff = repo.get_diff(
92 92 old_commit, new_commit, filenode_new.path,
93 93 ignore_whitespace, context, path1=filenode_old.path)
94 94 return vcs_gitdiff
95 95
96 96 NEW_FILENODE = 1
97 97 DEL_FILENODE = 2
98 98 MOD_FILENODE = 3
99 99 RENAMED_FILENODE = 4
100 100 COPIED_FILENODE = 5
101 101 CHMOD_FILENODE = 6
102 102 BIN_FILENODE = 7
103 103
104 104
105 105 class LimitedDiffContainer(object):
106 106
107 107 def __init__(self, diff_limit, cur_diff_size, diff):
108 108 self.diff = diff
109 109 self.diff_limit = diff_limit
110 110 self.cur_diff_size = cur_diff_size
111 111
112 112 def __getitem__(self, key):
113 113 return self.diff.__getitem__(key)
114 114
115 115 def __iter__(self):
116 116 for l in self.diff:
117 117 yield l
118 118
119 119
120 120 class Action(object):
121 121 """
122 122 Contains constants for the action value of the lines in a parsed diff.
123 123 """
124 124
125 125 ADD = 'add'
126 126 DELETE = 'del'
127 127 UNMODIFIED = 'unmod'
128 128
129 129 CONTEXT = 'context'
130 130 OLD_NO_NL = 'old-no-nl'
131 131 NEW_NO_NL = 'new-no-nl'
132 132
133 133
134 134 class DiffProcessor(object):
135 135 """
136 136 Give it a unified or git diff and it returns a list of the files that were
137 137 mentioned in the diff together with a dict of meta information that
138 138 can be used to render it in a HTML template.
139 139
140 140 .. note:: Unicode handling
141 141
142 142 The original diffs are a byte sequence and can contain filenames
143 143 in mixed encodings. This class generally returns `unicode` objects
144 144 since the result is intended for presentation to the user.
145 145
146 146 """
147 147 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
148 148 _newline_marker = re.compile(r'^\\ No newline at end of file')
149 149
150 150 # used for inline highlighter word split
151 151 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
152 152
153 153 # collapse ranges of commits over given number
154 154 _collapse_commits_over = 5
155 155
156 156 def __init__(self, diff, format='gitdiff', diff_limit=None,
157 157 file_limit=None, show_full_diff=True):
158 158 """
159 159 :param diff: A `Diff` object representing a diff from a vcs backend
160 160 :param format: format of diff passed, `udiff` or `gitdiff`
161 161 :param diff_limit: define the size of diff that is considered "big"
162 162 based on that parameter cut off will be triggered, set to None
163 163 to show full diff
164 164 """
165 165 self._diff = diff
166 166 self._format = format
167 167 self.adds = 0
168 168 self.removes = 0
169 169 # calculate diff size
170 170 self.diff_limit = diff_limit
171 171 self.file_limit = file_limit
172 172 self.show_full_diff = show_full_diff
173 173 self.cur_diff_size = 0
174 174 self.parsed = False
175 175 self.parsed_diff = []
176 176
177 177 log.debug('Initialized DiffProcessor with %s mode', format)
178 178 if format == 'gitdiff':
179 179 self.differ = self._highlight_line_difflib
180 180 self._parser = self._parse_gitdiff
181 181 else:
182 182 self.differ = self._highlight_line_udiff
183 183 self._parser = self._new_parse_gitdiff
184 184
185 185 def _copy_iterator(self):
186 186 """
187 187 make a fresh copy of generator, we should not iterate thru
188 188 an original as it's needed for repeating operations on
189 189 this instance of DiffProcessor
190 190 """
191 191 self.__udiff, iterator_copy = tee(self.__udiff)
192 192 return iterator_copy
193 193
194 194 def _escaper(self, string):
195 195 """
196 196 Escaper for diff escapes special chars and checks the diff limit
197 197
198 198 :param string:
199 199 """
200 200 self.cur_diff_size += len(string)
201 201
202 202 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
203 203 raise DiffLimitExceeded('Diff Limit Exceeded')
204 204
205 205 return string \
206 206 .replace('&', '&amp;')\
207 207 .replace('<', '&lt;')\
208 208 .replace('>', '&gt;')
209 209
210 210 def _line_counter(self, l):
211 211 """
212 212 Checks each line and bumps total adds/removes for this diff
213 213
214 214 :param l:
215 215 """
216 216 if l.startswith('+') and not l.startswith('+++'):
217 217 self.adds += 1
218 218 elif l.startswith('-') and not l.startswith('---'):
219 219 self.removes += 1
220 220 return safe_unicode(l)
221 221
222 222 def _highlight_line_difflib(self, line, next_):
223 223 """
224 224 Highlight inline changes in both lines.
225 225 """
226 226
227 227 if line['action'] == Action.DELETE:
228 228 old, new = line, next_
229 229 else:
230 230 old, new = next_, line
231 231
232 232 oldwords = self._token_re.split(old['line'])
233 233 newwords = self._token_re.split(new['line'])
234 234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
235 235
236 236 oldfragments, newfragments = [], []
237 237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
238 238 oldfrag = ''.join(oldwords[i1:i2])
239 239 newfrag = ''.join(newwords[j1:j2])
240 240 if tag != 'equal':
241 241 if oldfrag:
242 242 oldfrag = '<del>%s</del>' % oldfrag
243 243 if newfrag:
244 244 newfrag = '<ins>%s</ins>' % newfrag
245 245 oldfragments.append(oldfrag)
246 246 newfragments.append(newfrag)
247 247
248 248 old['line'] = "".join(oldfragments)
249 249 new['line'] = "".join(newfragments)
250 250
251 251 def _highlight_line_udiff(self, line, next_):
252 252 """
253 253 Highlight inline changes in both lines.
254 254 """
255 255 start = 0
256 256 limit = min(len(line['line']), len(next_['line']))
257 257 while start < limit and line['line'][start] == next_['line'][start]:
258 258 start += 1
259 259 end = -1
260 260 limit -= start
261 261 while -end <= limit and line['line'][end] == next_['line'][end]:
262 262 end -= 1
263 263 end += 1
264 264 if start or end:
265 265 def do(l):
266 266 last = end + len(l['line'])
267 267 if l['action'] == Action.ADD:
268 268 tag = 'ins'
269 269 else:
270 270 tag = 'del'
271 271 l['line'] = '%s<%s>%s</%s>%s' % (
272 272 l['line'][:start],
273 273 tag,
274 274 l['line'][start:last],
275 275 tag,
276 276 l['line'][last:]
277 277 )
278 278 do(line)
279 279 do(next_)
280 280
281 281 def _clean_line(self, line, command):
282 282 if command in ['+', '-', ' ']:
283 283 # only modify the line if it's actually a diff thing
284 284 line = line[1:]
285 285 return line
286 286
287 287 def _parse_gitdiff(self, inline_diff=True):
288 288 _files = []
289 289 diff_container = lambda arg: arg
290 290
291 291 for chunk in self._diff.chunks():
292 292 head = chunk.header
293 293
294 294 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
295 295 raw_diff = chunk.raw
296 296 limited_diff = False
297 297 exceeds_limit = False
298 298
299 299 op = None
300 300 stats = {
301 301 'added': 0,
302 302 'deleted': 0,
303 303 'binary': False,
304 304 'ops': {},
305 305 }
306 306
307 307 if head['deleted_file_mode']:
308 308 op = OPS.DEL
309 309 stats['binary'] = True
310 310 stats['ops'][DEL_FILENODE] = 'deleted file'
311 311
312 312 elif head['new_file_mode']:
313 313 op = OPS.ADD
314 314 stats['binary'] = True
315 315 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
316 316 else: # modify operation, can be copy, rename or chmod
317 317
318 318 # CHMOD
319 319 if head['new_mode'] and head['old_mode']:
320 320 op = OPS.MOD
321 321 stats['binary'] = True
322 322 stats['ops'][CHMOD_FILENODE] = (
323 323 'modified file chmod %s => %s' % (
324 324 head['old_mode'], head['new_mode']))
325 325 # RENAME
326 326 if head['rename_from'] != head['rename_to']:
327 327 op = OPS.MOD
328 328 stats['binary'] = True
329 329 stats['ops'][RENAMED_FILENODE] = (
330 330 'file renamed from %s to %s' % (
331 331 head['rename_from'], head['rename_to']))
332 332 # COPY
333 333 if head.get('copy_from') and head.get('copy_to'):
334 334 op = OPS.MOD
335 335 stats['binary'] = True
336 336 stats['ops'][COPIED_FILENODE] = (
337 337 'file copied from %s to %s' % (
338 338 head['copy_from'], head['copy_to']))
339 339
340 340 # If our new parsed headers didn't match anything fallback to
341 341 # old style detection
342 342 if op is None:
343 343 if not head['a_file'] and head['b_file']:
344 344 op = OPS.ADD
345 345 stats['binary'] = True
346 346 stats['ops'][NEW_FILENODE] = 'new file'
347 347
348 348 elif head['a_file'] and not head['b_file']:
349 349 op = OPS.DEL
350 350 stats['binary'] = True
351 351 stats['ops'][DEL_FILENODE] = 'deleted file'
352 352
353 353 # it's not ADD not DELETE
354 354 if op is None:
355 355 op = OPS.MOD
356 356 stats['binary'] = True
357 357 stats['ops'][MOD_FILENODE] = 'modified file'
358 358
359 359 # a real non-binary diff
360 360 if head['a_file'] or head['b_file']:
361 361 try:
362 362 raw_diff, chunks, _stats = self._parse_lines(diff)
363 363 stats['binary'] = False
364 364 stats['added'] = _stats[0]
365 365 stats['deleted'] = _stats[1]
366 366 # explicit mark that it's a modified file
367 367 if op == OPS.MOD:
368 368 stats['ops'][MOD_FILENODE] = 'modified file'
369 369 exceeds_limit = len(raw_diff) > self.file_limit
370 370
371 371 # changed from _escaper function so we validate size of
372 372 # each file instead of the whole diff
373 373 # diff will hide big files but still show small ones
374 374 # from my tests, big files are fairly safe to be parsed
375 375 # but the browser is the bottleneck
376 376 if not self.show_full_diff and exceeds_limit:
377 377 raise DiffLimitExceeded('File Limit Exceeded')
378 378
379 379 except DiffLimitExceeded:
380 380 diff_container = lambda _diff: \
381 381 LimitedDiffContainer(
382 382 self.diff_limit, self.cur_diff_size, _diff)
383 383
384 384 exceeds_limit = len(raw_diff) > self.file_limit
385 385 limited_diff = True
386 386 chunks = []
387 387
388 388 else: # GIT format binary patch, or possibly empty diff
389 389 if head['bin_patch']:
390 390 # we have operation already extracted, but we mark simply
391 391 # it's a diff we wont show for binary files
392 392 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
393 393 chunks = []
394 394
395 395 if chunks and not self.show_full_diff and op == OPS.DEL:
396 396 # if not full diff mode show deleted file contents
397 397 # TODO: anderson: if the view is not too big, there is no way
398 398 # to see the content of the file
399 399 chunks = []
400 400
401 401 chunks.insert(0, [{
402 402 'old_lineno': '',
403 403 'new_lineno': '',
404 404 'action': Action.CONTEXT,
405 405 'line': msg,
406 406 } for _op, msg in stats['ops'].iteritems()
407 407 if _op not in [MOD_FILENODE]])
408 408
409 409 _files.append({
410 410 'filename': safe_unicode(head['b_path']),
411 411 'old_revision': head['a_blob_id'],
412 412 'new_revision': head['b_blob_id'],
413 413 'chunks': chunks,
414 414 'raw_diff': safe_unicode(raw_diff),
415 415 'operation': op,
416 416 'stats': stats,
417 417 'exceeds_limit': exceeds_limit,
418 418 'is_limited_diff': limited_diff,
419 419 })
420 420
421 421 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
422 422 OPS.DEL: 2}.get(info['operation'])
423 423
424 424 if not inline_diff:
425 425 return diff_container(sorted(_files, key=sorter))
426 426
427 427 # highlight inline changes
428 428 for diff_data in _files:
429 429 for chunk in diff_data['chunks']:
430 430 lineiter = iter(chunk)
431 431 try:
432 432 while 1:
433 433 line = lineiter.next()
434 434 if line['action'] not in (
435 435 Action.UNMODIFIED, Action.CONTEXT):
436 436 nextline = lineiter.next()
437 437 if nextline['action'] in ['unmod', 'context'] or \
438 438 nextline['action'] == line['action']:
439 439 continue
440 440 self.differ(line, nextline)
441 441 except StopIteration:
442 442 pass
443 443
444 444 return diff_container(sorted(_files, key=sorter))
445 445
446 446 def _check_large_diff(self):
447 447 if self.diff_limit:
448 448 log.debug('Checking if diff exceeds current diff_limit of %s', self.diff_limit)
449 449 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
450 450 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
451 451
452 452 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
453 453 def _new_parse_gitdiff(self, inline_diff=True):
454 454 _files = []
455 455
456 456 # this can be overriden later to a LimitedDiffContainer type
457 457 diff_container = lambda arg: arg
458 458
459 459 for chunk in self._diff.chunks():
460 460 head = chunk.header
461 461 log.debug('parsing diff %r', head)
462 462
463 463 raw_diff = chunk.raw
464 464 limited_diff = False
465 465 exceeds_limit = False
466 466
467 467 op = None
468 468 stats = {
469 469 'added': 0,
470 470 'deleted': 0,
471 471 'binary': False,
472 472 'old_mode': None,
473 473 'new_mode': None,
474 474 'ops': {},
475 475 }
476 476 if head['old_mode']:
477 477 stats['old_mode'] = head['old_mode']
478 478 if head['new_mode']:
479 479 stats['new_mode'] = head['new_mode']
480 480 if head['b_mode']:
481 481 stats['new_mode'] = head['b_mode']
482 482
483 483 # delete file
484 484 if head['deleted_file_mode']:
485 485 op = OPS.DEL
486 486 stats['binary'] = True
487 487 stats['ops'][DEL_FILENODE] = 'deleted file'
488 488
489 489 # new file
490 490 elif head['new_file_mode']:
491 491 op = OPS.ADD
492 492 stats['binary'] = True
493 493 stats['old_mode'] = None
494 494 stats['new_mode'] = head['new_file_mode']
495 495 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
496 496
497 497 # modify operation, can be copy, rename or chmod
498 498 else:
499 499 # CHMOD
500 500 if head['new_mode'] and head['old_mode']:
501 501 op = OPS.MOD
502 502 stats['binary'] = True
503 503 stats['ops'][CHMOD_FILENODE] = (
504 504 'modified file chmod %s => %s' % (
505 505 head['old_mode'], head['new_mode']))
506 506
507 507 # RENAME
508 508 if head['rename_from'] != head['rename_to']:
509 509 op = OPS.MOD
510 510 stats['binary'] = True
511 511 stats['renamed'] = (head['rename_from'], head['rename_to'])
512 512 stats['ops'][RENAMED_FILENODE] = (
513 513 'file renamed from %s to %s' % (
514 514 head['rename_from'], head['rename_to']))
515 515 # COPY
516 516 if head.get('copy_from') and head.get('copy_to'):
517 517 op = OPS.MOD
518 518 stats['binary'] = True
519 519 stats['copied'] = (head['copy_from'], head['copy_to'])
520 520 stats['ops'][COPIED_FILENODE] = (
521 521 'file copied from %s to %s' % (
522 522 head['copy_from'], head['copy_to']))
523 523
524 524 # If our new parsed headers didn't match anything fallback to
525 525 # old style detection
526 526 if op is None:
527 527 if not head['a_file'] and head['b_file']:
528 528 op = OPS.ADD
529 529 stats['binary'] = True
530 530 stats['new_file'] = True
531 531 stats['ops'][NEW_FILENODE] = 'new file'
532 532
533 533 elif head['a_file'] and not head['b_file']:
534 534 op = OPS.DEL
535 535 stats['binary'] = True
536 536 stats['ops'][DEL_FILENODE] = 'deleted file'
537 537
538 538 # it's not ADD not DELETE
539 539 if op is None:
540 540 op = OPS.MOD
541 541 stats['binary'] = True
542 542 stats['ops'][MOD_FILENODE] = 'modified file'
543 543
544 544 # a real non-binary diff
545 545 if head['a_file'] or head['b_file']:
546 546 # simulate splitlines, so we keep the line end part
547 547 diff = self.diff_splitter(chunk.diff)
548 548
549 549 # append each file to the diff size
550 550 raw_chunk_size = len(raw_diff)
551 551
552 552 exceeds_limit = raw_chunk_size > self.file_limit
553 553 self.cur_diff_size += raw_chunk_size
554 554
555 555 try:
556 556 # Check each file instead of the whole diff.
557 557 # Diff will hide big files but still show small ones.
558 558 # From the tests big files are fairly safe to be parsed
559 559 # but the browser is the bottleneck.
560 560 if not self.show_full_diff and exceeds_limit:
561 561 log.debug('File `%s` exceeds current file_limit of %s',
562 562 safe_unicode(head['b_path']), self.file_limit)
563 563 raise DiffLimitExceeded(
564 564 'File Limit %s Exceeded', self.file_limit)
565 565
566 566 self._check_large_diff()
567 567
568 568 raw_diff, chunks, _stats = self._new_parse_lines(diff)
569 569 stats['binary'] = False
570 570 stats['added'] = _stats[0]
571 571 stats['deleted'] = _stats[1]
572 572 # explicit mark that it's a modified file
573 573 if op == OPS.MOD:
574 574 stats['ops'][MOD_FILENODE] = 'modified file'
575 575
576 576 except DiffLimitExceeded:
577 577 diff_container = lambda _diff: \
578 578 LimitedDiffContainer(
579 579 self.diff_limit, self.cur_diff_size, _diff)
580 580
581 581 limited_diff = True
582 582 chunks = []
583 583
584 584 else: # GIT format binary patch, or possibly empty diff
585 585 if head['bin_patch']:
586 586 # we have operation already extracted, but we mark simply
587 587 # it's a diff we wont show for binary files
588 588 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
589 589 chunks = []
590 590
591 591 # Hide content of deleted node by setting empty chunks
592 592 if chunks and not self.show_full_diff and op == OPS.DEL:
593 593 # if not full diff mode show deleted file contents
594 594 # TODO: anderson: if the view is not too big, there is no way
595 595 # to see the content of the file
596 596 chunks = []
597 597
598 598 chunks.insert(
599 599 0, [{'old_lineno': '',
600 600 'new_lineno': '',
601 601 'action': Action.CONTEXT,
602 602 'line': msg,
603 603 } for _op, msg in stats['ops'].iteritems()
604 604 if _op not in [MOD_FILENODE]])
605 605
606 606 original_filename = safe_unicode(head['a_path'])
607 607 _files.append({
608 608 'original_filename': original_filename,
609 609 'filename': safe_unicode(head['b_path']),
610 610 'old_revision': head['a_blob_id'],
611 611 'new_revision': head['b_blob_id'],
612 612 'chunks': chunks,
613 613 'raw_diff': safe_unicode(raw_diff),
614 614 'operation': op,
615 615 'stats': stats,
616 616 'exceeds_limit': exceeds_limit,
617 617 'is_limited_diff': limited_diff,
618 618 })
619 619
620 620 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
621 621 OPS.DEL: 2}.get(info['operation'])
622 622
623 623 return diff_container(sorted(_files, key=sorter))
624 624
625 625 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
626 626 def _parse_lines(self, diff_iter):
627 627 """
628 628 Parse the diff an return data for the template.
629 629 """
630 630
631 631 stats = [0, 0]
632 632 chunks = []
633 633 raw_diff = []
634 634
635 635 try:
636 636 line = diff_iter.next()
637 637
638 638 while line:
639 639 raw_diff.append(line)
640 640 lines = []
641 641 chunks.append(lines)
642 642
643 643 match = self._chunk_re.match(line)
644 644
645 645 if not match:
646 646 break
647 647
648 648 gr = match.groups()
649 649 (old_line, old_end,
650 650 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
651 651 old_line -= 1
652 652 new_line -= 1
653 653
654 654 context = len(gr) == 5
655 655 old_end += old_line
656 656 new_end += new_line
657 657
658 658 if context:
659 659 # skip context only if it's first line
660 660 if int(gr[0]) > 1:
661 661 lines.append({
662 662 'old_lineno': '...',
663 663 'new_lineno': '...',
664 664 'action': Action.CONTEXT,
665 665 'line': line,
666 666 })
667 667
668 668 line = diff_iter.next()
669 669
670 670 while old_line < old_end or new_line < new_end:
671 671 command = ' '
672 672 if line:
673 673 command = line[0]
674 674
675 675 affects_old = affects_new = False
676 676
677 677 # ignore those if we don't expect them
678 678 if command in '#@':
679 679 continue
680 680 elif command == '+':
681 681 affects_new = True
682 682 action = Action.ADD
683 683 stats[0] += 1
684 684 elif command == '-':
685 685 affects_old = True
686 686 action = Action.DELETE
687 687 stats[1] += 1
688 688 else:
689 689 affects_old = affects_new = True
690 690 action = Action.UNMODIFIED
691 691
692 692 if not self._newline_marker.match(line):
693 693 old_line += affects_old
694 694 new_line += affects_new
695 695 lines.append({
696 696 'old_lineno': affects_old and old_line or '',
697 697 'new_lineno': affects_new and new_line or '',
698 698 'action': action,
699 699 'line': self._clean_line(line, command)
700 700 })
701 701 raw_diff.append(line)
702 702
703 703 line = diff_iter.next()
704 704
705 705 if self._newline_marker.match(line):
706 706 # we need to append to lines, since this is not
707 707 # counted in the line specs of diff
708 708 lines.append({
709 709 'old_lineno': '...',
710 710 'new_lineno': '...',
711 711 'action': Action.CONTEXT,
712 712 'line': self._clean_line(line, command)
713 713 })
714 714
715 715 except StopIteration:
716 716 pass
717 717 return ''.join(raw_diff), chunks, stats
718 718
719 719 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
720 720 def _new_parse_lines(self, diff_iter):
721 721 """
722 722 Parse the diff an return data for the template.
723 723 """
724 724
725 725 stats = [0, 0]
726 726 chunks = []
727 727 raw_diff = []
728 728
729 729 try:
730 730 line = diff_iter.next()
731 731
732 732 while line:
733 733 raw_diff.append(line)
734 734 # match header e.g @@ -0,0 +1 @@\n'
735 735 match = self._chunk_re.match(line)
736 736
737 737 if not match:
738 738 break
739 739
740 740 gr = match.groups()
741 741 (old_line, old_end,
742 742 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
743 743
744 744 lines = []
745 745 hunk = {
746 746 'section_header': gr[-1],
747 747 'source_start': old_line,
748 748 'source_length': old_end,
749 749 'target_start': new_line,
750 750 'target_length': new_end,
751 751 'lines': lines,
752 752 }
753 753 chunks.append(hunk)
754 754
755 755 old_line -= 1
756 756 new_line -= 1
757 757
758 758 context = len(gr) == 5
759 759 old_end += old_line
760 760 new_end += new_line
761 761
762 762 line = diff_iter.next()
763 763
764 764 while old_line < old_end or new_line < new_end:
765 765 command = ' '
766 766 if line:
767 767 command = line[0]
768 768
769 769 affects_old = affects_new = False
770 770
771 771 # ignore those if we don't expect them
772 772 if command in '#@':
773 773 continue
774 774 elif command == '+':
775 775 affects_new = True
776 776 action = Action.ADD
777 777 stats[0] += 1
778 778 elif command == '-':
779 779 affects_old = True
780 780 action = Action.DELETE
781 781 stats[1] += 1
782 782 else:
783 783 affects_old = affects_new = True
784 784 action = Action.UNMODIFIED
785 785
786 786 if not self._newline_marker.match(line):
787 787 old_line += affects_old
788 788 new_line += affects_new
789 789 lines.append({
790 790 'old_lineno': affects_old and old_line or '',
791 791 'new_lineno': affects_new and new_line or '',
792 792 'action': action,
793 793 'line': self._clean_line(line, command)
794 794 })
795 795 raw_diff.append(line)
796 796
797 797 line = diff_iter.next()
798 798
799 799 if self._newline_marker.match(line):
800 800 # we need to append to lines, since this is not
801 801 # counted in the line specs of diff
802 802 if affects_old:
803 803 action = Action.OLD_NO_NL
804 804 elif affects_new:
805 805 action = Action.NEW_NO_NL
806 806 else:
807 807 raise Exception('invalid context for no newline')
808 808
809 809 lines.append({
810 810 'old_lineno': None,
811 811 'new_lineno': None,
812 812 'action': action,
813 813 'line': self._clean_line(line, command)
814 814 })
815 815
816 816 except StopIteration:
817 817 pass
818 818
819 819 return ''.join(raw_diff), chunks, stats
820 820
821 821 def _safe_id(self, idstring):
822 822 """Make a string safe for including in an id attribute.
823 823
824 824 The HTML spec says that id attributes 'must begin with
825 825 a letter ([A-Za-z]) and may be followed by any number
826 826 of letters, digits ([0-9]), hyphens ("-"), underscores
827 827 ("_"), colons (":"), and periods (".")'. These regexps
828 828 are slightly over-zealous, in that they remove colons
829 829 and periods unnecessarily.
830 830
831 831 Whitespace is transformed into underscores, and then
832 832 anything which is not a hyphen or a character that
833 833 matches \w (alphanumerics and underscore) is removed.
834 834
835 835 """
836 836 # Transform all whitespace to underscore
837 837 idstring = re.sub(r'\s', "_", '%s' % idstring)
838 838 # Remove everything that is not a hyphen or a member of \w
839 839 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
840 840 return idstring
841 841
842 842 @classmethod
843 843 def diff_splitter(cls, string):
844 844 """
845 845 Diff split that emulates .splitlines() but works only on \n
846 846 """
847 847 if not string:
848 848 return
849 849 elif string == '\n':
850 850 yield u'\n'
851 851 else:
852 852
853 853 has_newline = string.endswith('\n')
854 854 elements = string.split('\n')
855 855 if has_newline:
856 856 # skip last element as it's empty string from newlines
857 857 elements = elements[:-1]
858 858
859 859 len_elements = len(elements)
860 860
861 861 for cnt, line in enumerate(elements, start=1):
862 862 last_line = cnt == len_elements
863 863 if last_line and not has_newline:
864 864 yield safe_unicode(line)
865 865 else:
866 866 yield safe_unicode(line) + '\n'
867 867
868 868 def prepare(self, inline_diff=True):
869 869 """
870 870 Prepare the passed udiff for HTML rendering.
871 871
872 872 :return: A list of dicts with diff information.
873 873 """
874 874 parsed = self._parser(inline_diff=inline_diff)
875 875 self.parsed = True
876 876 self.parsed_diff = parsed
877 877 return parsed
878 878
879 879 def as_raw(self, diff_lines=None):
880 880 """
881 881 Returns raw diff as a byte string
882 882 """
883 883 return self._diff.raw
884 884
885 885 def as_html(self, table_class='code-difftable', line_class='line',
886 886 old_lineno_class='lineno old', new_lineno_class='lineno new',
887 887 code_class='code', enable_comments=False, parsed_lines=None):
888 888 """
889 889 Return given diff as html table with customized css classes
890 890 """
891 891 # TODO(marcink): not sure how to pass in translator
892 892 # here in an efficient way, leave the _ for proper gettext extraction
893 893 _ = lambda s: s
894 894
895 895 def _link_to_if(condition, label, url):
896 896 """
897 897 Generates a link if condition is meet or just the label if not.
898 898 """
899 899
900 900 if condition:
901 901 return '''<a href="%(url)s" class="tooltip"
902 902 title="%(title)s">%(label)s</a>''' % {
903 903 'title': _('Click to select line'),
904 904 'url': url,
905 905 'label': label
906 906 }
907 907 else:
908 908 return label
909 909 if not self.parsed:
910 910 self.prepare()
911 911
912 912 diff_lines = self.parsed_diff
913 913 if parsed_lines:
914 914 diff_lines = parsed_lines
915 915
916 916 _html_empty = True
917 917 _html = []
918 918 _html.append('''<table class="%(table_class)s">\n''' % {
919 919 'table_class': table_class
920 920 })
921 921
922 922 for diff in diff_lines:
923 923 for line in diff['chunks']:
924 924 _html_empty = False
925 925 for change in line:
926 926 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
927 927 'lc': line_class,
928 928 'action': change['action']
929 929 })
930 930 anchor_old_id = ''
931 931 anchor_new_id = ''
932 932 anchor_old = "%(filename)s_o%(oldline_no)s" % {
933 933 'filename': self._safe_id(diff['filename']),
934 934 'oldline_no': change['old_lineno']
935 935 }
936 936 anchor_new = "%(filename)s_n%(oldline_no)s" % {
937 937 'filename': self._safe_id(diff['filename']),
938 938 'oldline_no': change['new_lineno']
939 939 }
940 940 cond_old = (change['old_lineno'] != '...' and
941 941 change['old_lineno'])
942 942 cond_new = (change['new_lineno'] != '...' and
943 943 change['new_lineno'])
944 944 if cond_old:
945 945 anchor_old_id = 'id="%s"' % anchor_old
946 946 if cond_new:
947 947 anchor_new_id = 'id="%s"' % anchor_new
948 948
949 949 if change['action'] != Action.CONTEXT:
950 950 anchor_link = True
951 951 else:
952 952 anchor_link = False
953 953
954 954 ###########################################################
955 955 # COMMENT ICONS
956 956 ###########################################################
957 957 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
958 958
959 959 if enable_comments and change['action'] != Action.CONTEXT:
960 960 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
961 961
962 962 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
963 963
964 964 ###########################################################
965 965 # OLD LINE NUMBER
966 966 ###########################################################
967 967 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
968 968 'a_id': anchor_old_id,
969 969 'olc': old_lineno_class
970 970 })
971 971
972 972 _html.append('''%(link)s''' % {
973 973 'link': _link_to_if(anchor_link, change['old_lineno'],
974 974 '#%s' % anchor_old)
975 975 })
976 976 _html.append('''</td>\n''')
977 977 ###########################################################
978 978 # NEW LINE NUMBER
979 979 ###########################################################
980 980
981 981 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
982 982 'a_id': anchor_new_id,
983 983 'nlc': new_lineno_class
984 984 })
985 985
986 986 _html.append('''%(link)s''' % {
987 987 'link': _link_to_if(anchor_link, change['new_lineno'],
988 988 '#%s' % anchor_new)
989 989 })
990 990 _html.append('''</td>\n''')
991 991 ###########################################################
992 992 # CODE
993 993 ###########################################################
994 994 code_classes = [code_class]
995 995 if (not enable_comments or
996 996 change['action'] == Action.CONTEXT):
997 997 code_classes.append('no-comment')
998 998 _html.append('\t<td class="%s">' % ' '.join(code_classes))
999 999 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
1000 1000 'code': change['line']
1001 1001 })
1002 1002
1003 1003 _html.append('''\t</td>''')
1004 1004 _html.append('''\n</tr>\n''')
1005 1005 _html.append('''</table>''')
1006 1006 if _html_empty:
1007 1007 return None
1008 1008 return ''.join(_html)
1009 1009
1010 1010 def stat(self):
1011 1011 """
1012 1012 Returns tuple of added, and removed lines for this instance
1013 1013 """
1014 1014 return self.adds, self.removes
1015 1015
1016 1016 def get_context_of_line(
1017 1017 self, path, diff_line=None, context_before=3, context_after=3):
1018 1018 """
1019 1019 Returns the context lines for the specified diff line.
1020 1020
1021 1021 :type diff_line: :class:`DiffLineNumber`
1022 1022 """
1023 1023 assert self.parsed, "DiffProcessor is not initialized."
1024 1024
1025 1025 if None not in diff_line:
1026 1026 raise ValueError(
1027 1027 "Cannot specify both line numbers: {}".format(diff_line))
1028 1028
1029 1029 file_diff = self._get_file_diff(path)
1030 1030 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1031 1031
1032 1032 first_line_to_include = max(idx - context_before, 0)
1033 1033 first_line_after_context = idx + context_after + 1
1034 1034 context_lines = chunk[first_line_to_include:first_line_after_context]
1035 1035
1036 1036 line_contents = [
1037 1037 _context_line(line) for line in context_lines
1038 1038 if _is_diff_content(line)]
1039 1039 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1040 1040 # Once they are fixed, we can drop this line here.
1041 1041 if line_contents:
1042 1042 line_contents[-1] = (
1043 1043 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1044 1044 return line_contents
1045 1045
1046 1046 def find_context(self, path, context, offset=0):
1047 1047 """
1048 1048 Finds the given `context` inside of the diff.
1049 1049
1050 1050 Use the parameter `offset` to specify which offset the target line has
1051 1051 inside of the given `context`. This way the correct diff line will be
1052 1052 returned.
1053 1053
1054 1054 :param offset: Shall be used to specify the offset of the main line
1055 1055 within the given `context`.
1056 1056 """
1057 1057 if offset < 0 or offset >= len(context):
1058 1058 raise ValueError(
1059 1059 "Only positive values up to the length of the context "
1060 1060 "minus one are allowed.")
1061 1061
1062 1062 matches = []
1063 1063 file_diff = self._get_file_diff(path)
1064 1064
1065 1065 for chunk in file_diff['chunks']:
1066 1066 context_iter = iter(context)
1067 1067 for line_idx, line in enumerate(chunk):
1068 1068 try:
1069 1069 if _context_line(line) == context_iter.next():
1070 1070 continue
1071 1071 except StopIteration:
1072 1072 matches.append((line_idx, chunk))
1073 1073 context_iter = iter(context)
1074 1074
1075 1075 # Increment position and triger StopIteration
1076 1076 # if we had a match at the end
1077 1077 line_idx += 1
1078 1078 try:
1079 1079 context_iter.next()
1080 1080 except StopIteration:
1081 1081 matches.append((line_idx, chunk))
1082 1082
1083 1083 effective_offset = len(context) - offset
1084 1084 found_at_diff_lines = [
1085 1085 _line_to_diff_line_number(chunk[idx - effective_offset])
1086 1086 for idx, chunk in matches]
1087 1087
1088 1088 return found_at_diff_lines
1089 1089
1090 1090 def _get_file_diff(self, path):
1091 1091 for file_diff in self.parsed_diff:
1092 1092 if file_diff['filename'] == path:
1093 1093 break
1094 1094 else:
1095 1095 raise FileNotInDiffException("File {} not in diff".format(path))
1096 1096 return file_diff
1097 1097
1098 1098 def _find_chunk_line_index(self, file_diff, diff_line):
1099 1099 for chunk in file_diff['chunks']:
1100 1100 for idx, line in enumerate(chunk):
1101 1101 if line['old_lineno'] == diff_line.old:
1102 1102 return chunk, idx
1103 1103 if line['new_lineno'] == diff_line.new:
1104 1104 return chunk, idx
1105 1105 raise LineNotInDiffException(
1106 1106 "The line {} is not part of the diff.".format(diff_line))
1107 1107
1108 1108
1109 1109 def _is_diff_content(line):
1110 1110 return line['action'] in (
1111 1111 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1112 1112
1113 1113
1114 1114 def _context_line(line):
1115 1115 return (line['action'], line['line'])
1116 1116
1117 1117
1118 1118 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1119 1119
1120 1120
1121 1121 def _line_to_diff_line_number(line):
1122 1122 new_line_no = line['new_lineno'] or None
1123 1123 old_line_no = line['old_lineno'] or None
1124 1124 return DiffLineNumber(old=old_line_no, new=new_line_no)
1125 1125
1126 1126
1127 1127 class FileNotInDiffException(Exception):
1128 1128 """
1129 1129 Raised when the context for a missing file is requested.
1130 1130
1131 1131 If you request the context for a line in a file which is not part of the
1132 1132 given diff, then this exception is raised.
1133 1133 """
1134 1134
1135 1135
1136 1136 class LineNotInDiffException(Exception):
1137 1137 """
1138 1138 Raised when the context for a missing line is requested.
1139 1139
1140 1140 If you request the context for a line in a file and this line is not
1141 1141 part of the given diff, then this exception is raised.
1142 1142 """
1143 1143
1144 1144
1145 1145 class DiffLimitExceeded(Exception):
1146 1146 pass
1147 1147
1148 1148
1149 1149 # NOTE(marcink): if diffs.mako change, probably this
1150 1150 # needs a bump to next version
1151 CURRENT_DIFF_VERSION = 'v4'
1151 CURRENT_DIFF_VERSION = 'v5'
1152 1152
1153 1153
1154 1154 def _cleanup_cache_file(cached_diff_file):
1155 1155 # cleanup file to not store it "damaged"
1156 1156 try:
1157 1157 os.remove(cached_diff_file)
1158 1158 except Exception:
1159 1159 log.exception('Failed to cleanup path %s', cached_diff_file)
1160 1160
1161 1161
1162 1162 def _get_compression_mode(cached_diff_file):
1163 1163 mode = 'bz2'
1164 1164 if 'mode:plain' in cached_diff_file:
1165 1165 mode = 'plain'
1166 1166 elif 'mode:gzip' in cached_diff_file:
1167 1167 mode = 'gzip'
1168 1168 return mode
1169 1169
1170 1170
1171 1171 def cache_diff(cached_diff_file, diff, commits):
1172 1172 compression_mode = _get_compression_mode(cached_diff_file)
1173 1173
1174 1174 struct = {
1175 1175 'version': CURRENT_DIFF_VERSION,
1176 1176 'diff': diff,
1177 1177 'commits': commits
1178 1178 }
1179 1179
1180 1180 start = time.time()
1181 1181 try:
1182 1182 if compression_mode == 'plain':
1183 1183 with open(cached_diff_file, 'wb') as f:
1184 1184 pickle.dump(struct, f)
1185 1185 elif compression_mode == 'gzip':
1186 1186 with gzip.GzipFile(cached_diff_file, 'wb') as f:
1187 1187 pickle.dump(struct, f)
1188 1188 else:
1189 1189 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1190 1190 pickle.dump(struct, f)
1191 1191 except Exception:
1192 1192 log.warn('Failed to save cache', exc_info=True)
1193 1193 _cleanup_cache_file(cached_diff_file)
1194 1194
1195 1195 log.debug('Saved diff cache under %s in %.4fs', cached_diff_file, time.time() - start)
1196 1196
1197 1197
1198 1198 def load_cached_diff(cached_diff_file):
1199 1199 compression_mode = _get_compression_mode(cached_diff_file)
1200 1200
1201 1201 default_struct = {
1202 1202 'version': CURRENT_DIFF_VERSION,
1203 1203 'diff': None,
1204 1204 'commits': None
1205 1205 }
1206 1206
1207 1207 has_cache = os.path.isfile(cached_diff_file)
1208 1208 if not has_cache:
1209 1209 log.debug('Reading diff cache file failed %s', cached_diff_file)
1210 1210 return default_struct
1211 1211
1212 1212 data = None
1213 1213
1214 1214 start = time.time()
1215 1215 try:
1216 1216 if compression_mode == 'plain':
1217 1217 with open(cached_diff_file, 'rb') as f:
1218 1218 data = pickle.load(f)
1219 1219 elif compression_mode == 'gzip':
1220 1220 with gzip.GzipFile(cached_diff_file, 'rb') as f:
1221 1221 data = pickle.load(f)
1222 1222 else:
1223 1223 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1224 1224 data = pickle.load(f)
1225 1225 except Exception:
1226 1226 log.warn('Failed to read diff cache file', exc_info=True)
1227 1227
1228 1228 if not data:
1229 1229 data = default_struct
1230 1230
1231 1231 if not isinstance(data, dict):
1232 1232 # old version of data ?
1233 1233 data = default_struct
1234 1234
1235 1235 # check version
1236 1236 if data.get('version') != CURRENT_DIFF_VERSION:
1237 1237 # purge cache
1238 1238 _cleanup_cache_file(cached_diff_file)
1239 1239 return default_struct
1240 1240
1241 1241 log.debug('Loaded diff cache from %s in %.4fs', cached_diff_file, time.time() - start)
1242 1242
1243 1243 return data
1244 1244
1245 1245
1246 1246 def generate_diff_cache_key(*args):
1247 1247 """
1248 1248 Helper to generate a cache key using arguments
1249 1249 """
1250 1250 def arg_mapper(input_param):
1251 1251 input_param = safe_str(input_param)
1252 1252 # we cannot allow '/' in arguments since it would allow
1253 1253 # subdirectory usage
1254 1254 input_param.replace('/', '_')
1255 1255 return input_param or None # prevent empty string arguments
1256 1256
1257 1257 return '_'.join([
1258 1258 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1259 1259
1260 1260
1261 1261 def diff_cache_exist(cache_storage, *args):
1262 1262 """
1263 1263 Based on all generated arguments check and return a cache path
1264 1264 """
1265 1265 args = list(args) + ['mode:gzip']
1266 1266 cache_key = generate_diff_cache_key(*args)
1267 1267 cache_file_path = os.path.join(cache_storage, cache_key)
1268 1268 # prevent path traversal attacks using some param that have e.g '../../'
1269 1269 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1270 1270 raise ValueError('Final path must be within {}'.format(cache_storage))
1271 1271
1272 1272 return cache_file_path
@@ -1,600 +1,600 b''
1 1
2 2
3 3 //BUTTONS
4 4 button,
5 5 .btn,
6 6 input[type="button"] {
7 7 -webkit-appearance: none;
8 8 display: inline-block;
9 9 margin: 0 @padding/3 0 0;
10 10 padding: @button-padding;
11 11 text-align: center;
12 12 font-size: @basefontsize;
13 13 line-height: 1em;
14 14 font-family: @text-light;
15 15 text-decoration: none;
16 16 text-shadow: none;
17 17 color: @grey2;
18 18 background-color: white;
19 19 background-image: none;
20 20 border: none;
21 21 .border ( @border-thickness-buttons, @grey5 );
22 22 .border-radius (@border-radius);
23 23 cursor: pointer;
24 24 white-space: nowrap;
25 25 -webkit-transition: background .3s,color .3s;
26 26 -moz-transition: background .3s,color .3s;
27 27 -o-transition: background .3s,color .3s;
28 28 transition: background .3s,color .3s;
29 29 box-shadow: @button-shadow;
30 30 -webkit-box-shadow: @button-shadow;
31 31
32 32
33 33
34 34 a {
35 35 display: block;
36 36 margin: 0;
37 37 padding: 0;
38 38 color: inherit;
39 39 text-decoration: none;
40 40
41 41 &:hover {
42 42 text-decoration: none;
43 43 }
44 44 }
45 45
46 46 &:focus,
47 47 &:active {
48 48 outline:none;
49 49 }
50 50
51 51 &:hover {
52 52 color: @rcdarkblue;
53 53 background-color: @grey6;
54 54
55 55 }
56 56
57 57 &.btn-active {
58 58 color: @rcdarkblue;
59 59 background-color: @grey6;
60 60 }
61 61
62 62 .icon-remove {
63 63 display: none;
64 64 }
65 65
66 66 //disabled buttons
67 67 //last; overrides any other styles
68 68 &:disabled {
69 69 opacity: .7;
70 70 cursor: auto;
71 71 background-color: white;
72 72 color: @grey4;
73 73 text-shadow: none;
74 74 }
75 75
76 76 &.no-margin {
77 77 margin: 0 0 0 0;
78 78 }
79 79
80 80
81 81
82 82 }
83 83
84 84
85 85 .btn-default {
86 86 border: @border-thickness solid @grey5;
87 87 background-image: none;
88 88 color: @grey2;
89 89
90 90 a {
91 91 color: @grey2;
92 92 }
93 93
94 94 &:hover,
95 95 &.active {
96 96 color: @rcdarkblue;
97 97 background-color: @white;
98 98 .border ( @border-thickness, @grey4 );
99 99
100 100 a {
101 101 color: @grey2;
102 102 }
103 103 }
104 104 &:disabled {
105 105 .border ( @border-thickness-buttons, @grey5 );
106 106 background-color: transparent;
107 107 }
108 108 &.btn-active {
109 109 color: @rcdarkblue;
110 110 background-color: @white;
111 111 .border ( @border-thickness, @rcdarkblue );
112 112 }
113 113 }
114 114
115 115 .btn-primary,
116 116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
117 117 .btn-success {
118 118 .border ( @border-thickness, @rcblue );
119 119 background-color: @rcblue;
120 120 color: white;
121 121
122 122 a {
123 123 color: white;
124 124 }
125 125
126 126 &:hover,
127 127 &.active {
128 128 .border ( @border-thickness, @rcdarkblue );
129 129 color: white;
130 130 background-color: @rcdarkblue;
131 131
132 132 a {
133 133 color: white;
134 134 }
135 135 }
136 136 &:disabled {
137 137 background-color: @rcblue;
138 138 }
139 139 }
140 140
141 141 .btn-secondary {
142 142 &:extend(.btn-default);
143 143
144 144 background-color: white;
145 145
146 146 &:focus {
147 147 outline: 0;
148 148 }
149 149
150 150 &:hover {
151 151 &:extend(.btn-default:hover);
152 152 }
153 153
154 154 &.btn-link {
155 155 &:extend(.btn-link);
156 156 color: @rcblue;
157 157 }
158 158
159 159 &:disabled {
160 160 color: @rcblue;
161 161 background-color: white;
162 162 }
163 163 }
164 164
165 165 .btn-danger,
166 166 .revoke_perm,
167 167 .btn-x,
168 168 .form .action_button.btn-x {
169 169 .border ( @border-thickness, @alert2 );
170 170 background-color: white;
171 171 color: @alert2;
172 172
173 173 a {
174 174 color: @alert2;
175 175 }
176 176
177 177 &:hover,
178 178 &.active {
179 179 .border ( @border-thickness, @alert2 );
180 180 color: white;
181 181 background-color: @alert2;
182 182
183 183 a {
184 184 color: white;
185 185 }
186 186 }
187 187
188 188 i {
189 189 display:none;
190 190 }
191 191
192 192 &:disabled {
193 193 background-color: white;
194 194 color: @alert2;
195 195 }
196 196 }
197 197
198 198 .btn-warning {
199 199 .border ( @border-thickness, @alert3 );
200 200 background-color: white;
201 201 color: @alert3;
202 202
203 203 a {
204 204 color: @alert3;
205 205 }
206 206
207 207 &:hover,
208 208 &.active {
209 209 .border ( @border-thickness, @alert3 );
210 210 color: white;
211 211 background-color: @alert3;
212 212
213 213 a {
214 214 color: white;
215 215 }
216 216 }
217 217
218 218 i {
219 219 display:none;
220 220 }
221 221
222 222 &:disabled {
223 223 background-color: white;
224 224 color: @alert3;
225 225 }
226 226 }
227 227
228 228 .btn-approved-status {
229 229 .border ( @border-thickness, @alert1 );
230 230 background-color: white;
231 231 color: @alert1;
232 232
233 233 }
234 234
235 235 .btn-rejected-status {
236 236 .border ( @border-thickness, @alert2 );
237 237 background-color: white;
238 238 color: @alert2;
239 239 }
240 240
241 241 .btn-sm,
242 242 .btn-mini,
243 243 .field-sm .btn {
244 244 padding: @padding/3;
245 245 }
246 246
247 247 .btn-xs {
248 248 padding: @padding/4;
249 249 }
250 250
251 251 .btn-lg {
252 252 padding: @padding * 1.2;
253 253 }
254 254
255 255 .btn-group {
256 256 display: inline-block;
257 257 .btn {
258 258 float: left;
259 259 margin: 0 0 0 0;
260 260 // first item
261 261 &:first-of-type:not(:last-of-type) {
262 262 border-radius: @border-radius 0 0 @border-radius;
263 263
264 264 }
265 265 // middle elements
266 266 &:not(:first-of-type):not(:last-of-type) {
267 267 border-radius: 0;
268 268 border-left-width: 0;
269 269 border-right-width: 0;
270 270 }
271 271 // last item
272 272 &:last-of-type:not(:first-of-type) {
273 273 border-radius: 0 @border-radius @border-radius 0;
274 274 }
275 275
276 276 &:only-child {
277 277 border-radius: @border-radius;
278 278 }
279 279 }
280 280
281 281 }
282 282
283 283
284 284 .btn-group-actions {
285 285 position: relative;
286 286 z-index: 50;
287 287
288 288 &:not(.open) .btn-action-switcher-container {
289 289 display: none;
290 290 }
291 291
292 292 .btn-more-option {
293 293 margin-left: -1px;
294 294 padding-left: 2px;
295 295 padding-right: 2px;
296 296 }
297 297 }
298 298
299 299
300 300 .btn-action-switcher-container {
301 301 position: absolute;
302 302 top: 100%;
303 303
304 304 &.left-align {
305 305 left: 0;
306 306 }
307 307 &.right-align {
308 308 right: 0;
309 309 }
310 310
311 311 }
312 312
313 313 .btn-action-switcher {
314 314 display: block;
315 315 position: relative;
316 316 z-index: 300;
317 317 max-width: 600px;
318 318 margin-top: 4px;
319 319 margin-bottom: 24px;
320 320 font-size: 14px;
321 321 font-weight: 400;
322 322 padding: 8px 0;
323 323 background-color: #fff;
324 324 border: 1px solid @grey4;
325 325 border-radius: 3px;
326 326 box-shadow: @dropdown-shadow;
327 327 overflow: auto;
328 328
329 329 li {
330 330 display: block;
331 331 text-align: left;
332 332 list-style: none;
333 333 padding: 5px 10px;
334 334 }
335 335
336 336 li .action-help-block {
337 337 font-size: 10px;
338 338 line-height: normal;
339 339 color: @grey4;
340 340 }
341 341
342 342 }
343 343
344 344 .btn-link {
345 345 background: transparent;
346 346 border: none;
347 347 padding: 0;
348 348 color: @rcblue;
349 349
350 350 &:hover {
351 351 background: transparent;
352 352 border: none;
353 353 color: @rcdarkblue;
354 354 }
355 355
356 356 //disabled buttons
357 357 //last; overrides any other styles
358 358 &:disabled {
359 359 opacity: .7;
360 360 cursor: auto;
361 361 background-color: white;
362 362 color: @grey4;
363 363 text-shadow: none;
364 364 }
365 365
366 366 // TODO: johbo: Check if we can avoid this, indicates that the structure
367 367 // is not yet good.
368 368 // lisa: The button CSS reflects the button HTML; both need a cleanup.
369 369 &.btn-danger {
370 370 color: @alert2;
371 371
372 372 &:hover {
373 color: darken(@alert2,30%);
373 color: darken(@alert2, 30%);
374 374 }
375 375
376 376 &:disabled {
377 377 color: @alert2;
378 378 }
379 379 }
380 380 }
381 381
382 382 .btn-social {
383 383 &:extend(.btn-default);
384 384 margin: 5px 5px 5px 0px;
385 385 min-width: 160px;
386 386 }
387 387
388 388 // TODO: johbo: check these exceptions
389 389
390 390 .links {
391 391
392 392 .btn + .btn {
393 393 margin-top: @padding;
394 394 }
395 395 }
396 396
397 397
398 398 .action_button {
399 399 display:inline;
400 400 margin: 0;
401 401 padding: 0 1em 0 0;
402 402 font-size: inherit;
403 403 color: @rcblue;
404 404 border: none;
405 405 border-radius: 0;
406 406 background-color: transparent;
407 407
408 408 &.last-item {
409 409 border: none;
410 410 padding: 0 0 0 0;
411 411 }
412 412
413 413 &:last-child {
414 414 border: none;
415 415 padding: 0 0 0 0;
416 416 }
417 417
418 418 &:hover {
419 419 color: @rcdarkblue;
420 420 background-color: transparent;
421 421 border: none;
422 422 }
423 423 .noselect
424 424 }
425 425
426 426 .grid_delete {
427 427 .action_button {
428 428 border: none;
429 429 }
430 430 }
431 431
432 432
433 433 input[type="submit"].btn-warning {
434 434 &:extend(.btn-warning);
435 435
436 436 &:focus {
437 437 outline: 0;
438 438 }
439 439
440 440 &:hover {
441 441 &:extend(.btn-warning:hover);
442 442 }
443 443
444 444 &.btn-link {
445 445 &:extend(.btn-link);
446 446 color: @alert3;
447 447
448 448 &:disabled {
449 449 color: @alert3;
450 450 background-color: transparent;
451 451 }
452 452 }
453 453
454 454 &:disabled {
455 455 .border ( @border-thickness-buttons, @alert3 );
456 456 background-color: white;
457 457 color: @alert3;
458 458 opacity: 0.5;
459 459 }
460 460 }
461 461
462 462
463 463
464 464 // TODO: johbo: Form button tweaks, check if we can use the classes instead
465 465 input[type="submit"] {
466 466 &:extend(.btn-primary);
467 467
468 468 &:focus {
469 469 outline: 0;
470 470 }
471 471
472 472 &:hover {
473 473 &:extend(.btn-primary:hover);
474 474 }
475 475
476 476 &.btn-link {
477 477 &:extend(.btn-link);
478 478 color: @rcblue;
479 479
480 480 &:disabled {
481 481 color: @rcblue;
482 482 background-color: transparent;
483 483 }
484 484 }
485 485
486 486 &:disabled {
487 487 .border ( @border-thickness-buttons, @rcblue );
488 488 background-color: @rcblue;
489 489 color: white;
490 490 opacity: 0.5;
491 491 }
492 492 }
493 493
494 494 input[type="reset"] {
495 495 &:extend(.btn-default);
496 496
497 497 // TODO: johbo: Check if this tweak can be avoided.
498 498 background: transparent;
499 499
500 500 &:focus {
501 501 outline: 0;
502 502 }
503 503
504 504 &:hover {
505 505 &:extend(.btn-default:hover);
506 506 }
507 507
508 508 &.btn-link {
509 509 &:extend(.btn-link);
510 510 color: @rcblue;
511 511
512 512 &:disabled {
513 513 border: none;
514 514 }
515 515 }
516 516
517 517 &:disabled {
518 518 .border ( @border-thickness-buttons, @rcblue );
519 519 background-color: white;
520 520 color: @rcblue;
521 521 }
522 522 }
523 523
524 524 input[type="submit"],
525 525 input[type="reset"] {
526 526 &.btn-danger {
527 527 &:extend(.btn-danger);
528 528
529 529 &:focus {
530 530 outline: 0;
531 531 }
532 532
533 533 &:hover {
534 534 &:extend(.btn-danger:hover);
535 535 }
536 536
537 537 &.btn-link {
538 538 &:extend(.btn-link);
539 539 color: @alert2;
540 540
541 541 &:hover {
542 542 color: darken(@alert2,30%);
543 543 }
544 544 }
545 545
546 546 &:disabled {
547 547 color: @alert2;
548 548 background-color: white;
549 549 }
550 550 }
551 551 &.btn-danger-action {
552 552 .border ( @border-thickness, @alert2 );
553 553 background-color: @alert2;
554 554 color: white;
555 555
556 556 a {
557 557 color: white;
558 558 }
559 559
560 560 &:hover {
561 561 background-color: darken(@alert2,20%);
562 562 }
563 563
564 564 &.active {
565 565 .border ( @border-thickness, @alert2 );
566 566 color: white;
567 567 background-color: @alert2;
568 568
569 569 a {
570 570 color: white;
571 571 }
572 572 }
573 573
574 574 &:disabled {
575 575 background-color: white;
576 576 color: @alert2;
577 577 }
578 578 }
579 579 }
580 580
581 581
582 582 .button-links {
583 583 float: left;
584 584 display: inline;
585 585 margin: 0;
586 586 padding-left: 0;
587 587 list-style: none;
588 588 text-align: right;
589 589
590 590 li {
591 591
592 592
593 593 }
594 594
595 595 li.active {
596 596 background-color: @grey6;
597 597 .border ( @border-thickness, @grey4 );
598 598 }
599 599
600 600 }
@@ -1,1347 +1,1347 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21
22 22 div.diffblock .sidebyside {
23 23 background: #ffffff;
24 24 }
25 25
26 26 div.diffblock {
27 27 overflow-x: auto;
28 28 overflow-y: hidden;
29 29 clear: both;
30 30 padding: 0px;
31 31 background: @grey6;
32 32 border: @border-thickness solid @grey5;
33 33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
34 34 border-radius: @border-radius @border-radius 0px 0px;
35 35
36 36
37 37 .comments-number {
38 38 float: right;
39 39 }
40 40
41 41 // BEGIN CODE-HEADER STYLES
42 42
43 43 .code-header {
44 44 background: @grey6;
45 45 padding: 10px 0 10px 0;
46 46 height: auto;
47 47 width: 100%;
48 48
49 49 .hash {
50 50 float: left;
51 51 padding: 2px 0 0 2px;
52 52 }
53 53
54 54 .date {
55 55 float: left;
56 56 text-transform: uppercase;
57 57 padding: 4px 0px 0px 2px;
58 58 }
59 59
60 60 div {
61 61 margin-left: 4px;
62 62 }
63 63
64 64 div.compare_header {
65 65 min-height: 40px;
66 66 margin: 0;
67 67 padding: 0 @padding;
68 68
69 69 .drop-menu {
70 70 float:left;
71 71 display: block;
72 72 margin:0 0 @padding 0;
73 73 }
74 74
75 75 .compare-label {
76 76 float: left;
77 77 clear: both;
78 78 display: inline-block;
79 79 min-width: 5em;
80 80 margin: 0;
81 81 padding: @button-padding @button-padding @button-padding 0;
82 82 font-weight: @text-semibold-weight;
83 83 font-family: @text-semibold;
84 84 }
85 85
86 86 .compare-buttons {
87 87 float: left;
88 88 margin: 0;
89 89 padding: 0 0 @padding;
90 90
91 91 .btn {
92 92 margin: 0 @padding 0 0;
93 93 }
94 94 }
95 95 }
96 96
97 97 }
98 98
99 99 .parents {
100 100 float: left;
101 101 width: 100px;
102 102 font-weight: 400;
103 103 vertical-align: middle;
104 104 padding: 0px 2px 0px 2px;
105 105 background-color: @grey6;
106 106
107 107 #parent_link {
108 108 margin: 00px 2px;
109 109
110 110 &.double {
111 111 margin: 0px 2px;
112 112 }
113 113
114 114 &.disabled{
115 115 margin-right: @padding;
116 116 }
117 117 }
118 118 }
119 119
120 120 .children {
121 121 float: right;
122 122 width: 100px;
123 123 font-weight: 400;
124 124 vertical-align: middle;
125 125 text-align: right;
126 126 padding: 0px 2px 0px 2px;
127 127 background-color: @grey6;
128 128
129 129 #child_link {
130 130 margin: 0px 2px;
131 131
132 132 &.double {
133 133 margin: 0px 2px;
134 134 }
135 135
136 136 &.disabled{
137 137 margin-right: @padding;
138 138 }
139 139 }
140 140 }
141 141
142 142 .changeset_header {
143 143 height: 16px;
144 144
145 145 & > div{
146 146 margin-right: @padding;
147 147 }
148 148 }
149 149
150 150 .changeset_file {
151 151 text-align: left;
152 152 float: left;
153 153 padding: 0;
154 154
155 155 a{
156 156 display: inline-block;
157 157 margin-right: 0.5em;
158 158 }
159 159
160 160 #selected_mode{
161 161 margin-left: 0;
162 162 }
163 163 }
164 164
165 165 .diff-menu-wrapper {
166 166 float: left;
167 167 }
168 168
169 169 .diff-menu {
170 170 position: absolute;
171 171 background: none repeat scroll 0 0 #FFFFFF;
172 172 border-color: #003367 @grey3 @grey3;
173 173 border-right: 1px solid @grey3;
174 174 border-style: solid solid solid;
175 175 border-width: @border-thickness;
176 176 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
177 177 margin-top: 5px;
178 178 margin-left: 1px;
179 179 }
180 180
181 181 .diff-actions, .editor-actions {
182 182 float: left;
183 183
184 184 input{
185 185 margin: 0 0.5em 0 0;
186 186 }
187 187 }
188 188
189 189 // END CODE-HEADER STYLES
190 190
191 191 // BEGIN CODE-BODY STYLES
192 192
193 193 .code-body {
194 194 padding: 0;
195 195 background-color: #ffffff;
196 196 position: relative;
197 197 max-width: none;
198 198 box-sizing: border-box;
199 199 // TODO: johbo: Parent has overflow: auto, this forces the child here
200 200 // to have the intended size and to scroll. Should be simplified.
201 201 width: 100%;
202 202 overflow-x: auto;
203 203 }
204 204
205 205 pre.raw {
206 206 background: white;
207 207 color: @grey1;
208 208 }
209 209 // END CODE-BODY STYLES
210 210
211 211 }
212 212
213 213
214 214 table.code-difftable {
215 215 border-collapse: collapse;
216 216 width: 99%;
217 217 border-radius: 0px !important;
218 218
219 219 td {
220 220 padding: 0 !important;
221 221 background: none !important;
222 222 border: 0 !important;
223 223 }
224 224
225 225 .context {
226 226 background: none repeat scroll 0 0 #DDE7EF;
227 227 }
228 228
229 229 .add {
230 230 background: none repeat scroll 0 0 #DDFFDD;
231 231
232 232 ins {
233 233 background: none repeat scroll 0 0 #AAFFAA;
234 234 text-decoration: none;
235 235 }
236 236 }
237 237
238 238 .del {
239 239 background: none repeat scroll 0 0 #FFDDDD;
240 240
241 241 del {
242 242 background: none repeat scroll 0 0 #FFAAAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 /** LINE NUMBERS **/
248 248 .lineno {
249 249 padding-left: 2px !important;
250 250 padding-right: 2px;
251 251 text-align: right;
252 252 width: 32px;
253 253 -moz-user-select: none;
254 254 -webkit-user-select: none;
255 255 border-right: @border-thickness solid @grey5 !important;
256 256 border-left: 0px solid #CCC !important;
257 257 border-top: 0px solid #CCC !important;
258 258 border-bottom: none !important;
259 259
260 260 a {
261 261 &:extend(pre);
262 262 text-align: right;
263 263 padding-right: 2px;
264 264 cursor: pointer;
265 265 display: block;
266 266 width: 32px;
267 267 }
268 268 }
269 269
270 270 .context {
271 271 cursor: auto;
272 272 &:extend(pre);
273 273 }
274 274
275 275 .lineno-inline {
276 276 background: none repeat scroll 0 0 #FFF !important;
277 277 padding-left: 2px;
278 278 padding-right: 2px;
279 279 text-align: right;
280 280 width: 30px;
281 281 -moz-user-select: none;
282 282 -webkit-user-select: none;
283 283 }
284 284
285 285 /** CODE **/
286 286 .code {
287 287 display: block;
288 288 width: 100%;
289 289
290 290 td {
291 291 margin: 0;
292 292 padding: 0;
293 293 }
294 294
295 295 pre {
296 296 margin: 0;
297 297 padding: 0;
298 298 margin-left: .5em;
299 299 }
300 300 }
301 301 }
302 302
303 303
304 304 // Comments
305 305 .comment-selected-hl {
306 306 border-left: 6px solid @comment-highlight-color !important;
307 307 padding-left: 3px !important;
308 308 margin-left: -7px !important;
309 309 }
310 310
311 311 div.comment:target,
312 312 div.comment-outdated:target {
313 313 .comment-selected-hl;
314 314 }
315 315
316 316 //TODO: anderson: can't get an absolute number out of anything, so had to put the
317 317 //current values that might change. But to make it clear I put as a calculation
318 318 @comment-max-width: 1065px;
319 319 @pr-extra-margin: 34px;
320 320 @pr-border-spacing: 4px;
321 321 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
322 322
323 323 // Pull Request
324 324 .cs_files .code-difftable {
325 325 border: @border-thickness solid @grey5; //borders only on PRs
326 326
327 327 .comment-inline-form,
328 328 div.comment {
329 329 width: @pr-comment-width;
330 330 }
331 331 }
332 332
333 333 // Changeset
334 334 .code-difftable {
335 335 .comment-inline-form,
336 336 div.comment {
337 337 width: @comment-max-width;
338 338 }
339 339 }
340 340
341 341 //Style page
342 342 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
343 343 #style-page .code-difftable{
344 344 .comment-inline-form,
345 345 div.comment {
346 346 width: @comment-max-width - @style-extra-margin;
347 347 }
348 348 }
349 349
350 350 #context-bar > h2 {
351 351 font-size: 20px;
352 352 }
353 353
354 354 #context-bar > h2> a {
355 355 font-size: 20px;
356 356 }
357 357 // end of defaults
358 358
359 359 .file_diff_buttons {
360 360 padding: 0 0 @padding;
361 361
362 362 .drop-menu {
363 363 float: left;
364 364 margin: 0 @padding 0 0;
365 365 }
366 366 .btn {
367 367 margin: 0 @padding 0 0;
368 368 }
369 369 }
370 370
371 371 .code-body.textarea.editor {
372 372 max-width: none;
373 373 padding: 15px;
374 374 }
375 375
376 376 td.injected_diff{
377 377 max-width: 1178px;
378 378 overflow-x: auto;
379 379 overflow-y: hidden;
380 380
381 381 div.diff-container,
382 382 div.diffblock{
383 383 max-width: 100%;
384 384 }
385 385
386 386 div.code-body {
387 387 max-width: 1124px;
388 388 overflow-x: auto;
389 389 overflow-y: hidden;
390 390 padding: 0;
391 391 }
392 392 div.diffblock {
393 393 border: none;
394 394 }
395 395
396 396 &.inline-form {
397 397 width: 99%
398 398 }
399 399 }
400 400
401 401
402 402 table.code-difftable {
403 403 width: 100%;
404 404 }
405 405
406 406 /** PYGMENTS COLORING **/
407 407 div.codeblock {
408 408
409 409 // TODO: johbo: Added interim to get rid of the margin around
410 410 // Select2 widgets. This needs further cleanup.
411 411 overflow: auto;
412 412 padding: 0px;
413 413 border: @border-thickness solid @grey6;
414 414 .border-radius(@border-radius);
415 415
416 416 #remove_gist {
417 417 float: right;
418 418 }
419 419
420 420 .gist_url {
421 421 padding: 0px 0px 35px 0px;
422 422 }
423 423
424 424 .gist-desc {
425 425 clear: both;
426 426 margin: 0 0 10px 0;
427 427 code {
428 428 white-space: pre-line;
429 429 line-height: inherit
430 430 }
431 431 }
432 432
433 433 .author {
434 434 clear: both;
435 435 vertical-align: middle;
436 436 font-weight: @text-bold-weight;
437 437 font-family: @text-bold;
438 438 }
439 439
440 440 .btn-mini {
441 441 float: left;
442 442 margin: 0 5px 0 0;
443 443 }
444 444
445 445 .code-header {
446 446 padding: @padding;
447 447 border-bottom: @border-thickness solid @grey5;
448 448
449 449 .rc-user {
450 450 min-width: 0;
451 451 margin-right: .5em;
452 452 }
453 453
454 454 .stats {
455 455 clear: both;
456 456 margin: 0 0 @padding 0;
457 457 padding: 0;
458 458 .left {
459 459 float: left;
460 460 clear: left;
461 461 max-width: 75%;
462 462 margin: 0 0 @padding 0;
463 463
464 464 &.item {
465 465 margin-right: @padding;
466 466 &.last { border-right: none; }
467 467 }
468 468 }
469 469 .buttons { float: right; }
470 470 .author {
471 471 height: 25px; margin-left: 15px; font-weight: bold;
472 472 }
473 473 }
474 474
475 475 .commit {
476 476 margin: 5px 0 0 26px;
477 477 font-weight: normal;
478 478 white-space: pre-wrap;
479 479 }
480 480 }
481 481
482 482 .message {
483 483 position: relative;
484 484 margin: @padding;
485 485
486 486 .codeblock-label {
487 487 margin: 0 0 1em 0;
488 488 }
489 489 }
490 490
491 491 .code-body {
492 492 padding: 0.8em 1em;
493 493 background-color: #ffffff;
494 494 min-width: 100%;
495 495 box-sizing: border-box;
496 496 // TODO: johbo: Parent has overflow: auto, this forces the child here
497 497 // to have the intended size and to scroll. Should be simplified.
498 498 width: 100%;
499 499 overflow-x: auto;
500 500
501 501 img.rendered-binary {
502 502 height: auto;
503 503 width: auto;
504 504 }
505 505
506 506 .markdown-block {
507 507 padding: 1em 0;
508 508 }
509 509 }
510 510
511 511 .codeblock-header {
512 512 background: @grey7;
513 513 height: 36px;
514 514 }
515 515
516 516 .path {
517 517 border-bottom: 1px solid @grey6;
518 518 padding: .65em 1em;
519 519 height: 18px;
520 520 }
521 521 }
522 522
523 523 .code-highlighttable,
524 524 div.codeblock {
525 525
526 526 &.readme {
527 527 background-color: white;
528 528 }
529 529
530 530 .markdown-block table {
531 531 border-collapse: collapse;
532 532
533 533 th,
534 534 td {
535 535 padding: .5em;
536 536 border: @border-thickness solid @border-default-color;
537 537 }
538 538 }
539 539
540 540 table {
541 541 border: 0px;
542 542 margin: 0;
543 543 letter-spacing: normal;
544 544
545 545
546 546 td {
547 547 border: 0px;
548 548 vertical-align: top;
549 549 }
550 550 }
551 551 }
552 552
553 553 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
554 554 div.search-code-body {
555 555 background-color: #ffffff; padding: 5px 0 5px 10px;
556 556 pre {
557 557 .match { background-color: #faffa6;}
558 558 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
559 559 }
560 560 .code-highlighttable {
561 561 border-collapse: collapse;
562 562
563 563 tr:hover {
564 564 background: #fafafa;
565 565 }
566 566 td.code {
567 567 padding-left: 10px;
568 568 }
569 569 td.line {
570 570 border-right: 1px solid #ccc !important;
571 571 padding-right: 10px;
572 572 text-align: right;
573 573 font-family: @text-monospace;
574 574 span {
575 575 white-space: pre-wrap;
576 576 color: #666666;
577 577 }
578 578 }
579 579 }
580 580 }
581 581
582 582 div.annotatediv { margin-left: 2px; margin-right: 4px; }
583 583 .code-highlight {
584 584 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
585 585 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
586 586 pre div:target {background-color: @comment-highlight-color !important;}
587 587 }
588 588
589 589 .linenos a { text-decoration: none; }
590 590
591 591 .CodeMirror-selected { background: @rchighlightblue; }
592 592 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
593 593 .CodeMirror ::selection { background: @rchighlightblue; }
594 594 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
595 595
596 596 .code { display: block; border:0px !important; }
597 597
598 598 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
599 599 .codehilite {
600 600 /*ElasticMatch is custom RhodeCode TAG*/
601 601
602 602 .c-ElasticMatch {
603 603 background-color: #faffa6;
604 604 padding: 0.2em;
605 605 }
606 606 }
607 607
608 608 /* This can be generated with `pygmentize -S default -f html` */
609 609 .code-highlight,
610 610 .codehilite {
611 611 /*ElasticMatch is custom RhodeCode TAG*/
612 612 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
613 613 .hll { background-color: #ffffcc }
614 614 .c { color: #408080; font-style: italic } /* Comment */
615 615 .err, .codehilite .err { border: none } /* Error */
616 616 .k { color: #008000; font-weight: bold } /* Keyword */
617 617 .o { color: #666666 } /* Operator */
618 618 .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
619 619 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
620 620 .cp { color: #BC7A00 } /* Comment.Preproc */
621 621 .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
622 622 .c1 { color: #408080; font-style: italic } /* Comment.Single */
623 623 .cs { color: #408080; font-style: italic } /* Comment.Special */
624 624 .gd { color: #A00000 } /* Generic.Deleted */
625 625 .ge { font-style: italic } /* Generic.Emph */
626 626 .gr { color: #FF0000 } /* Generic.Error */
627 627 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
628 628 .gi { color: #00A000 } /* Generic.Inserted */
629 629 .go { color: #888888 } /* Generic.Output */
630 630 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
631 631 .gs { font-weight: bold } /* Generic.Strong */
632 632 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
633 633 .gt { color: #0044DD } /* Generic.Traceback */
634 634 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
635 635 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
636 636 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
637 637 .kp { color: #008000 } /* Keyword.Pseudo */
638 638 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
639 639 .kt { color: #B00040 } /* Keyword.Type */
640 640 .m { color: #666666 } /* Literal.Number */
641 641 .s { color: #BA2121 } /* Literal.String */
642 642 .na { color: #7D9029 } /* Name.Attribute */
643 643 .nb { color: #008000 } /* Name.Builtin */
644 644 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
645 645 .no { color: #880000 } /* Name.Constant */
646 646 .nd { color: #AA22FF } /* Name.Decorator */
647 647 .ni { color: #999999; font-weight: bold } /* Name.Entity */
648 648 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
649 649 .nf { color: #0000FF } /* Name.Function */
650 650 .nl { color: #A0A000 } /* Name.Label */
651 651 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
652 652 .nt { color: #008000; font-weight: bold } /* Name.Tag */
653 653 .nv { color: #19177C } /* Name.Variable */
654 654 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
655 655 .w { color: #bbbbbb } /* Text.Whitespace */
656 656 .mb { color: #666666 } /* Literal.Number.Bin */
657 657 .mf { color: #666666 } /* Literal.Number.Float */
658 658 .mh { color: #666666 } /* Literal.Number.Hex */
659 659 .mi { color: #666666 } /* Literal.Number.Integer */
660 660 .mo { color: #666666 } /* Literal.Number.Oct */
661 661 .sa { color: #BA2121 } /* Literal.String.Affix */
662 662 .sb { color: #BA2121 } /* Literal.String.Backtick */
663 663 .sc { color: #BA2121 } /* Literal.String.Char */
664 664 .dl { color: #BA2121 } /* Literal.String.Delimiter */
665 665 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
666 666 .s2 { color: #BA2121 } /* Literal.String.Double */
667 667 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
668 668 .sh { color: #BA2121 } /* Literal.String.Heredoc */
669 669 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
670 670 .sx { color: #008000 } /* Literal.String.Other */
671 671 .sr { color: #BB6688 } /* Literal.String.Regex */
672 672 .s1 { color: #BA2121 } /* Literal.String.Single */
673 673 .ss { color: #19177C } /* Literal.String.Symbol */
674 674 .bp { color: #008000 } /* Name.Builtin.Pseudo */
675 675 .fm { color: #0000FF } /* Name.Function.Magic */
676 676 .vc { color: #19177C } /* Name.Variable.Class */
677 677 .vg { color: #19177C } /* Name.Variable.Global */
678 678 .vi { color: #19177C } /* Name.Variable.Instance */
679 679 .vm { color: #19177C } /* Name.Variable.Magic */
680 680 .il { color: #666666 } /* Literal.Number.Integer.Long */
681 681
682 682 }
683 683
684 684 /* customized pre blocks for markdown/rst */
685 685 pre.literal-block, .codehilite pre{
686 686 padding: @padding;
687 687 border: 1px solid @grey6;
688 688 .border-radius(@border-radius);
689 689 background-color: @grey7;
690 690 }
691 691
692 692
693 693 /* START NEW CODE BLOCK CSS */
694 694
695 695 @cb-line-height: 18px;
696 696 @cb-line-code-padding: 10px;
697 697 @cb-text-padding: 5px;
698 698
699 699 @pill-padding: 2px 7px;
700 700 @pill-padding-small: 2px 2px 1px 2px;
701 701
702 702 input.filediff-collapse-state {
703 703 display: none;
704 704
705 705 &:checked + .filediff { /* file diff is collapsed */
706 706 .cb {
707 707 display: none
708 708 }
709 709 .filediff-collapse-indicator {
710 710 float: left;
711 711 cursor: pointer;
712 712 margin: 1px -5px;
713 713 }
714 714 .filediff-collapse-indicator:before {
715 715 content: '\f105';
716 716 }
717 717
718 718 .filediff-menu {
719 719 display: none;
720 720 }
721 721
722 722 }
723 723
724 724 &+ .filediff { /* file diff is expanded */
725 725
726 726 .filediff-collapse-indicator {
727 727 float: left;
728 728 cursor: pointer;
729 729 margin: 1px -5px;
730 730 }
731 731 .filediff-collapse-indicator:before {
732 732 content: '\f107';
733 733 }
734 734
735 735 .filediff-menu {
736 736 display: block;
737 737 }
738 738
739 739 margin: 10px 0;
740 740 &:nth-child(2) {
741 741 margin: 0;
742 742 }
743 743 }
744 744 }
745 745
746 746 .filediffs .anchor {
747 747 display: block;
748 748 height: 40px;
749 749 margin-top: -40px;
750 750 visibility: hidden;
751 751 }
752 752
753 753 .filediffs .anchor:nth-of-type(1) {
754 754 display: block;
755 755 height: 80px;
756 756 margin-top: -80px;
757 757 visibility: hidden;
758 758 }
759 759
760 760 .cs_files {
761 761 clear: both;
762 762 }
763 763
764 764 #diff-file-sticky{
765 765 will-change: min-height;
766 766 height: 80px;
767 767 }
768 768
769 769 .sidebar__inner{
770 770 transform: translate(0, 0); /* For browsers don't support translate3d. */
771 771 transform: translate3d(0, 0, 0);
772 772 will-change: position, transform;
773 773 height: 65px;
774 774 background-color: #fff;
775 775 padding: 5px 0px;
776 776 }
777 777
778 778 .sidebar__bar {
779 779 padding: 5px 0px 0px 0px
780 780 }
781 781
782 782 .fpath-placeholder {
783 783 clear: both;
784 784 visibility: hidden
785 785 }
786 786
787 787 .is-affixed {
788 788
789 789 .sidebar__inner {
790 790 z-index: 30;
791 791 }
792 792
793 793 .sidebar_inner_shadow {
794 794 position: fixed;
795 795 top: 75px;
796 796 right: -100%;
797 797 left: -100%;
798 798 z-index: 30;
799 799 display: block;
800 800 height: 5px;
801 801 content: "";
802 802 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
803 803 border-top: 1px solid rgba(0, 0, 0, 0.15);
804 804 }
805 805
806 806 .fpath-placeholder {
807 807 visibility: visible !important;
808 808 }
809 809 }
810 810
811 811 .diffset-menu {
812 812
813 813 }
814 814
815 815 #todo-box {
816 816 clear:both;
817 817 display: none;
818 818 text-align: right
819 819 }
820 820
821 821 .diffset {
822 822 margin: 0px auto;
823 823 .diffset-heading {
824 824 border: 1px solid @grey5;
825 825 margin-bottom: -1px;
826 826 // margin-top: 20px;
827 827 h2 {
828 828 margin: 0;
829 829 line-height: 38px;
830 830 padding-left: 10px;
831 831 }
832 832 .btn {
833 833 margin: 0;
834 834 }
835 835 background: @grey6;
836 836 display: block;
837 837 padding: 5px;
838 838 }
839 839 .diffset-heading-warning {
840 840 background: @alert3-inner;
841 841 border: 1px solid @alert3;
842 842 }
843 843 &.diffset-comments-disabled {
844 844 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
845 845 display: none !important;
846 846 }
847 847 }
848 848 }
849 849
850 850 .filelist {
851 851 .pill {
852 852 display: block;
853 853 float: left;
854 854 padding: @pill-padding-small;
855 855 }
856 856 }
857 857
858 858 .pill {
859 859 display: block;
860 860 float: left;
861 861 padding: @pill-padding;
862 862 }
863 863
864 864 .pill-group {
865 865 .pill {
866 866 opacity: .8;
867 867 margin-right: 3px;
868 868 font-size: 12px;
869 869 font-weight: normal;
870 870 min-width: 30px;
871 871 text-align: center;
872 872
873 873 &:first-child {
874 874 border-radius: @border-radius 0 0 @border-radius;
875 875 }
876 876 &:last-child {
877 877 border-radius: 0 @border-radius @border-radius 0;
878 878 }
879 879 &:only-child {
880 880 border-radius: @border-radius;
881 881 margin-right: 0;
882 882 }
883 883 }
884 884 }
885 885
886 886 /* Main comments*/
887 887 #comments {
888 888 .comment-selected {
889 889 border-left: 6px solid @comment-highlight-color;
890 890 padding-left: 3px;
891 891 margin-left: -9px;
892 892 }
893 893 }
894 894
895 895 .filediff {
896 896 border: 1px solid @grey5;
897 897
898 898 /* START OVERRIDES */
899 899 .code-highlight {
900 900 border: none; // TODO: remove this border from the global
901 901 // .code-highlight, it doesn't belong there
902 902 }
903 903 label {
904 904 margin: 0; // TODO: remove this margin definition from global label
905 905 // it doesn't belong there - if margin on labels
906 906 // are needed for a form they should be defined
907 907 // in the form's class
908 908 }
909 909 /* END OVERRIDES */
910 910
911 911 * {
912 912 box-sizing: border-box;
913 913 }
914 914
915 915 .on-hover-icon {
916 916 visibility: hidden;
917 917 }
918 918
919 919 .filediff-anchor {
920 920 visibility: hidden;
921 921 }
922 922 &:hover {
923 923 .filediff-anchor {
924 924 visibility: visible;
925 925 }
926 926 .on-hover-icon {
927 927 visibility: visible;
928 928 }
929 929 }
930 930
931 931 .filediff-heading {
932 932 cursor: pointer;
933 933 display: block;
934 934 padding: 10px 10px;
935 935 }
936 936 .filediff-heading:after {
937 937 content: "";
938 938 display: table;
939 939 clear: both;
940 940 }
941 941 .filediff-heading:hover {
942 942 background: #e1e9f4 !important;
943 943 }
944 944
945 945 .filediff-menu {
946 946 text-align: right;
947 947 padding: 5px 5px 5px 0px;
948 948 background: @grey7;
949 949
950 950 &> a,
951 951 &> span {
952 952 padding: 1px;
953 953 }
954 954 }
955 955
956 956 .filediff-collapse-button, .filediff-expand-button {
957 957 cursor: pointer;
958 958 }
959 959 .filediff-collapse-button {
960 960 display: inline;
961 961 }
962 962 .filediff-expand-button {
963 963 display: none;
964 964 }
965 965 .filediff-collapsed .filediff-collapse-button {
966 966 display: none;
967 967 }
968 968 .filediff-collapsed .filediff-expand-button {
969 969 display: inline;
970 970 }
971 971
972 972 /**** COMMENTS ****/
973 973
974 974 .filediff-menu {
975 975 .show-comment-button {
976 976 display: none;
977 977 }
978 978 }
979 979 &.hide-comments {
980 980 .inline-comments {
981 981 display: none;
982 982 }
983 983 .filediff-menu {
984 984 .show-comment-button {
985 985 display: inline;
986 986 }
987 987 .hide-comment-button {
988 988 display: none;
989 989 }
990 990 }
991 991 }
992 992
993 993 .hide-line-comments {
994 994 .inline-comments {
995 995 display: none;
996 996 }
997 997 }
998 998
999 999 /**** END COMMENTS ****/
1000 1000
1001 1001
1002 1002 .nav-chunk {
1003 1003 position: absolute;
1004 1004 right: 20px;
1005 margin-top: -17px;
1005 margin-top: -15px;
1006 1006 }
1007 1007
1008 1008 .nav-chunk.selected {
1009 1009 visibility: visible !important;
1010 1010 }
1011 1011
1012 1012 #diff_nav {
1013 1013 color: @grey3;
1014 1014 }
1015 1015
1016 1016 }
1017 1017
1018 1018
1019 1019 .op-added {
1020 1020 color: @alert1;
1021 1021 }
1022 1022
1023 1023 .op-deleted {
1024 1024 color: @alert2;
1025 1025 }
1026 1026
1027 1027 .filediff, .filelist {
1028 1028
1029 1029 .pill {
1030 1030 &[op="name"] {
1031 1031 background: none;
1032 1032 opacity: 1;
1033 1033 color: white;
1034 1034 }
1035 1035 &[op="limited"] {
1036 1036 background: @grey2;
1037 1037 color: white;
1038 1038 }
1039 1039 &[op="binary"] {
1040 1040 background: @color7;
1041 1041 color: white;
1042 1042 }
1043 1043 &[op="modified"] {
1044 1044 background: @alert1;
1045 1045 color: white;
1046 1046 }
1047 1047 &[op="renamed"] {
1048 1048 background: @color4;
1049 1049 color: white;
1050 1050 }
1051 1051 &[op="copied"] {
1052 1052 background: @color4;
1053 1053 color: white;
1054 1054 }
1055 1055 &[op="mode"] {
1056 1056 background: @grey3;
1057 1057 color: white;
1058 1058 }
1059 1059 &[op="symlink"] {
1060 1060 background: @color8;
1061 1061 color: white;
1062 1062 }
1063 1063
1064 1064 &[op="added"] { /* added lines */
1065 1065 background: @alert1;
1066 1066 color: white;
1067 1067 }
1068 1068 &[op="deleted"] { /* deleted lines */
1069 1069 background: @alert2;
1070 1070 color: white;
1071 1071 }
1072 1072
1073 1073 &[op="created"] { /* created file */
1074 1074 background: @alert1;
1075 1075 color: white;
1076 1076 }
1077 1077 &[op="removed"] { /* deleted file */
1078 1078 background: @color5;
1079 1079 color: white;
1080 1080 }
1081 1081
1082 1082 &[op="comments"] { /* comments on file */
1083 1083 background: @grey4;
1084 1084 color: white;
1085 1085 }
1086 1086
1087 1087 &[op="options"] { /* context menu */
1088 1088 background: @grey6;
1089 1089 color: black;
1090 1090 }
1091 1091 }
1092 1092 }
1093 1093
1094 1094
1095 1095 .filediff-outdated {
1096 1096 padding: 8px 0;
1097 1097
1098 1098 .filediff-heading {
1099 1099 opacity: .5;
1100 1100 }
1101 1101 }
1102 1102
1103 1103 table.cb {
1104 1104 width: 100%;
1105 1105 border-collapse: collapse;
1106 1106
1107 1107 .cb-text {
1108 1108 padding: @cb-text-padding;
1109 1109 }
1110 1110 .cb-hunk {
1111 1111 padding: @cb-text-padding;
1112 1112 }
1113 1113 .cb-expand {
1114 1114 display: none;
1115 1115 }
1116 1116 .cb-collapse {
1117 1117 display: inline;
1118 1118 }
1119 1119 &.cb-collapsed {
1120 1120 .cb-line {
1121 1121 display: none;
1122 1122 }
1123 1123 .cb-expand {
1124 1124 display: inline;
1125 1125 }
1126 1126 .cb-collapse {
1127 1127 display: none;
1128 1128 }
1129 1129 .cb-hunk {
1130 1130 display: none;
1131 1131 }
1132 1132 }
1133 1133
1134 1134 /* intentionally general selector since .cb-line-selected must override it
1135 1135 and they both use !important since the td itself may have a random color
1136 1136 generated by annotation blocks. TLDR: if you change it, make sure
1137 1137 annotated block selection and line selection in file view still work */
1138 1138 .cb-line-fresh .cb-content {
1139 1139 background: white !important;
1140 1140 }
1141 1141 .cb-warning {
1142 1142 background: #fff4dd;
1143 1143 }
1144 1144
1145 1145 &.cb-diff-sideside {
1146 1146 td {
1147 1147 &.cb-content {
1148 1148 width: 50%;
1149 1149 }
1150 1150 }
1151 1151 }
1152 1152
1153 1153 tr {
1154 1154 &.cb-annotate {
1155 1155 border-top: 1px solid #eee;
1156 1156 }
1157 1157
1158 1158 &.cb-comment-info {
1159 1159 border-top: 1px solid #eee;
1160 1160 color: rgba(0, 0, 0, 0.3);
1161 1161 background: #edf2f9;
1162 1162
1163 1163 td {
1164 1164
1165 1165 }
1166 1166 }
1167 1167
1168 1168 &.cb-hunk {
1169 1169 font-family: @text-monospace;
1170 1170 color: rgba(0, 0, 0, 0.3);
1171 1171
1172 1172 td {
1173 1173 &:first-child {
1174 1174 background: #edf2f9;
1175 1175 }
1176 1176 &:last-child {
1177 1177 background: #f4f7fb;
1178 1178 }
1179 1179 }
1180 1180 }
1181 1181 }
1182 1182
1183 1183
1184 1184 td {
1185 1185 vertical-align: top;
1186 1186 padding: 0;
1187 1187
1188 1188 &.cb-content {
1189 1189 font-size: 12.35px;
1190 1190
1191 1191 &.cb-line-selected .cb-code {
1192 1192 background: @comment-highlight-color !important;
1193 1193 }
1194 1194
1195 1195 span.cb-code {
1196 1196 line-height: @cb-line-height;
1197 1197 padding-left: @cb-line-code-padding;
1198 1198 padding-right: @cb-line-code-padding;
1199 1199 display: block;
1200 1200 white-space: pre-wrap;
1201 1201 font-family: @text-monospace;
1202 1202 word-break: break-all;
1203 1203 .nonl {
1204 1204 color: @color5;
1205 1205 }
1206 1206 .cb-action {
1207 1207 &:before {
1208 1208 content: " ";
1209 1209 }
1210 1210 &.cb-deletion:before {
1211 1211 content: "- ";
1212 1212 }
1213 1213 &.cb-addition:before {
1214 1214 content: "+ ";
1215 1215 }
1216 1216 }
1217 1217 }
1218 1218
1219 1219 &> button.cb-comment-box-opener {
1220 1220
1221 1221 padding: 2px 2px 1px 3px;
1222 1222 margin-left: -6px;
1223 1223 margin-top: -1px;
1224 1224
1225 1225 border-radius: @border-radius;
1226 1226 position: absolute;
1227 1227 display: none;
1228 1228 }
1229 1229 .cb-comment {
1230 1230 margin-top: 10px;
1231 1231 white-space: normal;
1232 1232 }
1233 1233 }
1234 1234 &:hover {
1235 1235 button.cb-comment-box-opener {
1236 1236 display: block;
1237 1237 }
1238 1238 &+ td button.cb-comment-box-opener {
1239 1239 display: block
1240 1240 }
1241 1241 }
1242 1242
1243 1243 &.cb-data {
1244 1244 text-align: right;
1245 1245 width: 30px;
1246 1246 font-family: @text-monospace;
1247 1247
1248 1248 .icon-comment {
1249 1249 cursor: pointer;
1250 1250 }
1251 1251 &.cb-line-selected {
1252 1252 background: @comment-highlight-color !important;
1253 1253 }
1254 1254 &.cb-line-selected > div {
1255 1255 display: block;
1256 1256 background: @comment-highlight-color !important;
1257 1257 line-height: @cb-line-height;
1258 1258 color: rgba(0, 0, 0, 0.3);
1259 1259 }
1260 1260 }
1261 1261
1262 1262 &.cb-lineno {
1263 1263 padding: 0;
1264 1264 width: 50px;
1265 1265 color: rgba(0, 0, 0, 0.3);
1266 1266 text-align: right;
1267 1267 border-right: 1px solid #eee;
1268 1268 font-family: @text-monospace;
1269 1269 -webkit-user-select: none;
1270 1270 -moz-user-select: none;
1271 1271 user-select: none;
1272 1272
1273 1273 a::before {
1274 1274 content: attr(data-line-no);
1275 1275 }
1276 1276 &.cb-line-selected {
1277 1277 background: @comment-highlight-color !important;
1278 1278 }
1279 1279
1280 1280 a {
1281 1281 display: block;
1282 1282 padding-right: @cb-line-code-padding;
1283 1283 padding-left: @cb-line-code-padding;
1284 1284 line-height: @cb-line-height;
1285 1285 color: rgba(0, 0, 0, 0.3);
1286 1286 }
1287 1287 }
1288 1288
1289 1289 &.cb-empty {
1290 1290 background: @grey7;
1291 1291 }
1292 1292
1293 1293 ins {
1294 1294 color: black;
1295 1295 background: #a6f3a6;
1296 1296 text-decoration: none;
1297 1297 }
1298 1298 del {
1299 1299 color: black;
1300 1300 background: #f8cbcb;
1301 1301 text-decoration: none;
1302 1302 }
1303 1303 &.cb-addition {
1304 1304 background: #ecffec;
1305 1305
1306 1306 &.blob-lineno {
1307 1307 background: #ddffdd;
1308 1308 }
1309 1309 }
1310 1310 &.cb-deletion {
1311 1311 background: #ffecec;
1312 1312
1313 1313 &.blob-lineno {
1314 1314 background: #ffdddd;
1315 1315 }
1316 1316 }
1317 1317 &.cb-annotate-message-spacer {
1318 1318 width:8px;
1319 1319 padding: 1px 0px 0px 3px;
1320 1320 }
1321 1321 &.cb-annotate-info {
1322 1322 width: 320px;
1323 1323 min-width: 320px;
1324 1324 max-width: 320px;
1325 1325 padding: 5px 2px;
1326 1326 font-size: 13px;
1327 1327
1328 1328 .cb-annotate-message {
1329 1329 padding: 2px 0px 0px 0px;
1330 1330 white-space: pre-line;
1331 1331 overflow: hidden;
1332 1332 }
1333 1333 .rc-user {
1334 1334 float: none;
1335 1335 padding: 0 6px 0 17px;
1336 1336 min-width: unset;
1337 1337 min-height: unset;
1338 1338 }
1339 1339 }
1340 1340
1341 1341 &.cb-annotate-revision {
1342 1342 cursor: pointer;
1343 1343 text-align: right;
1344 1344 padding: 1px 3px 0px 3px;
1345 1345 }
1346 1346 }
1347 1347 }
@@ -1,642 +1,746 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 @comment-outdated-opacity: 0.6;
7 @comment-outdated-opacity: 1.0;
8 8
9 9 .comments {
10 10 width: 100%;
11 11 }
12 12
13 13 .comments-heading {
14 14 margin-bottom: -1px;
15 15 background: @grey6;
16 16 display: block;
17 17 padding: 10px 0px;
18 18 font-size: 18px
19 19 }
20 20
21 21 #comment-tr-show {
22 22 padding: 5px 0;
23 23 }
24 24
25 25 tr.inline-comments div {
26 26 max-width: 100%;
27 27
28 28 p {
29 29 white-space: normal;
30 30 }
31 31
32 32 code, pre, .code, dd {
33 33 overflow-x: auto;
34 34 width: 1062px;
35 35 }
36 36
37 37 dd {
38 38 width: auto;
39 39 }
40 40 }
41 41
42 42 #injected_page_comments {
43 43 .comment-previous-link,
44 44 .comment-next-link,
45 45 .comment-links-divider {
46 46 display: none;
47 47 }
48 48 }
49 49
50 50 .add-comment {
51 51 margin-bottom: 10px;
52 52 }
53 53 .hide-comment-button .add-comment {
54 54 display: none;
55 55 }
56 56
57 57 .comment-bubble {
58 58 color: @grey4;
59 59 margin-top: 4px;
60 60 margin-right: 30px;
61 61 visibility: hidden;
62 62 }
63 63
64 64 .comment-draft {
65 65 float: left;
66 66 margin-right: 10px;
67 font-weight: 600;
68 color: @alert3;
67 font-weight: 400;
68 color: @color-draft;
69 }
70
71 .comment-new {
72 float: left;
73 margin-right: 10px;
74 font-weight: 400;
75 color: @color-new;
69 76 }
70 77
71 78 .comment-label {
72 79 float: left;
73 80
74 padding: 0.4em 0.4em;
75 margin: 2px 4px 0px 0px;
76 display: inline-block;
81 padding: 0 8px 0 0;
77 82 min-height: 0;
78 83
79 84 text-align: center;
80 85 font-size: 10px;
81 line-height: .8em;
82 86
83 87 font-family: @text-italic;
84 88 font-style: italic;
85 89 background: #fff none;
86 90 color: @grey3;
87 border: 1px solid @grey4;
88 91 white-space: nowrap;
89 92
90 93 text-transform: uppercase;
91 94 min-width: 50px;
92 border-radius: 4px;
93 95
94 96 &.todo {
95 97 color: @color5;
96 98 font-style: italic;
97 99 font-weight: @text-bold-italic-weight;
98 100 font-family: @text-bold-italic;
99 101 }
100 102
101 103 .resolve {
102 104 cursor: pointer;
103 105 text-decoration: underline;
104 106 }
105 107
106 108 .resolved {
107 109 text-decoration: line-through;
108 110 color: @color1;
109 111 }
110 112 .resolved a {
111 113 text-decoration: line-through;
112 114 color: @color1;
113 115 }
114 116 .resolve-text {
115 117 color: @color1;
116 118 margin: 2px 8px;
117 119 font-family: @text-italic;
118 120 font-style: italic;
119 121 }
120 122 }
121 123
122 124 .has-spacer-after {
123 125 &:after {
124 126 content: ' | ';
125 127 color: @grey5;
126 128 }
127 129 }
128 130
129 131 .has-spacer-before {
130 132 &:before {
131 133 content: ' | ';
132 134 color: @grey5;
133 135 }
134 136 }
135 137
136 138 .comment {
137 139
138 140 &.comment-general {
139 141 border: 1px solid @grey5;
140 142 padding: 5px 5px 5px 5px;
141 143 }
142 144
143 145 margin: @padding 0;
144 146 padding: 4px 0 0 0;
145 147 line-height: 1em;
146 148
147 149 .rc-user {
148 150 min-width: 0;
149 151 margin: 0px .5em 0 0;
150 152
151 153 .user {
152 154 display: inline;
153 155 }
154 156 }
155 157
156 158 .meta {
157 159 position: relative;
158 160 width: 100%;
159 161 border-bottom: 1px solid @grey5;
160 162 margin: -5px 0px;
161 163 line-height: 24px;
162 164
163 165 &:hover .permalink {
164 166 visibility: visible;
165 167 color: @rcblue;
166 168 }
167 169 }
168 170
169 171 .author,
170 172 .date {
171 173 display: inline;
172 174
173 175 &:after {
174 176 content: ' | ';
175 177 color: @grey5;
176 178 }
177 179 }
178 180
179 181 .author-general img {
180 182 top: 3px;
181 183 }
182 184 .author-inline img {
183 185 top: 3px;
184 186 }
185 187
186 188 .status-change,
187 189 .permalink,
188 190 .changeset-status-lbl {
189 191 display: inline;
190 192 }
191 193
192 194 .permalink {
193 195 visibility: hidden;
194 196 }
195 197
196 198 .comment-links-divider {
197 199 display: inline;
198 200 }
199 201
200 202 .comment-links-block {
201 203 float:right;
202 204 text-align: right;
203 205 min-width: 85px;
204 206
205 207 [class^="icon-"]:before,
206 208 [class*=" icon-"]:before {
207 209 margin-left: 0;
208 210 margin-right: 0;
209 211 }
210 212 }
211 213
212 214 .comment-previous-link {
213 215 display: inline-block;
214 216
215 217 .arrow_comment_link{
216 218 cursor: pointer;
217 219 i {
218 220 font-size:10px;
219 221 }
220 222 }
221 223 .arrow_comment_link.disabled {
222 224 cursor: default;
223 225 color: @grey5;
224 226 }
225 227 }
226 228
227 229 .comment-next-link {
228 230 display: inline-block;
229 231
230 232 .arrow_comment_link{
231 233 cursor: pointer;
232 234 i {
233 235 font-size:10px;
234 236 }
235 237 }
236 238 .arrow_comment_link.disabled {
237 239 cursor: default;
238 240 color: @grey5;
239 241 }
240 242 }
241 243
242 244 .delete-comment {
243 245 display: inline-block;
244 246 color: @rcblue;
245 247
246 248 &:hover {
247 249 cursor: pointer;
248 250 }
249 251 }
250 252
251 253 .text {
252 254 clear: both;
253 255 .border-radius(@border-radius);
254 256 .box-sizing(border-box);
255 257
256 258 .markdown-block p,
257 259 .rst-block p {
258 260 margin: .5em 0 !important;
259 261 // TODO: lisa: This is needed because of other rst !important rules :[
260 262 }
261 263 }
262 264
263 265 .pr-version {
264 266 display: inline-block;
265 267 }
266 268 .pr-version-inline {
267 269 display: inline-block;
268 270 }
269 271 .pr-version-num {
270 272 font-size: 10px;
271 273 }
272 274 }
273 275
274 276 @comment-padding: 5px;
275 277
276 278 .general-comments {
277 279 .comment-outdated {
278 280 opacity: @comment-outdated-opacity;
279 281 }
282
283 .comment-outdated-label {
284 color: @grey3;
285 padding-right: 4px;
286 }
280 287 }
281 288
282 289 .inline-comments {
283 border-radius: @border-radius;
290
284 291 .comment {
285 292 margin: 0;
286 border-radius: @border-radius;
287 293 }
294
288 295 .comment-outdated {
289 296 opacity: @comment-outdated-opacity;
290 297 }
291 298
299 .comment-outdated-label {
300 color: @grey3;
301 padding-right: 4px;
302 }
303
292 304 .comment-inline {
305
306 &:first-child {
307 margin: 4px 4px 0 4px;
308 border-top: 1px solid @grey5;
309 border-bottom: 0 solid @grey5;
310 border-left: 1px solid @grey5;
311 border-right: 1px solid @grey5;
312 .border-radius-top(4px);
313 }
314
315 &:only-child {
316 margin: 4px 4px 0 4px;
317 border-top: 1px solid @grey5;
318 border-bottom: 0 solid @grey5;
319 border-left: 1px solid @grey5;
320 border-right: 1px solid @grey5;
321 .border-radius-top(4px);
322 }
323
293 324 background: white;
294 325 padding: @comment-padding @comment-padding;
295 border: @comment-padding solid @grey6;
326 margin: 0 4px 0 4px;
327 border-top: 0 solid @grey5;
328 border-bottom: 0 solid @grey5;
329 border-left: 1px solid @grey5;
330 border-right: 1px solid @grey5;
296 331
297 332 .text {
298 333 border: none;
299 334 }
335
300 336 .meta {
301 337 border-bottom: 1px solid @grey6;
302 338 margin: -5px 0px;
303 339 line-height: 24px;
304 340 }
341
305 342 }
306 343 .comment-selected {
307 344 border-left: 6px solid @comment-highlight-color;
308 345 }
346
347 .comment-inline-form-open {
348 display: block !important;
349 }
350
309 351 .comment-inline-form {
310 padding: @comment-padding;
311 352 display: none;
312 353 }
313 .cb-comment-add-button {
314 margin: @comment-padding;
354
355 .comment-inline-form-edit {
356 padding: 0;
357 margin: 0px 4px 2px 4px;
358 }
359
360 .reply-thread-container {
361 display: table;
362 width: 100%;
363 padding: 0px 4px 4px 4px;
364 }
365
366 .reply-thread-container-wrapper {
367 margin: 0 4px 4px 4px;
368 border-top: 0 solid @grey5;
369 border-bottom: 1px solid @grey5;
370 border-left: 1px solid @grey5;
371 border-right: 1px solid @grey5;
372 .border-radius-bottom(4px);
373 }
374
375 .reply-thread-gravatar {
376 display: table-cell;
377 width: 24px;
378 height: 24px;
379 padding-top: 10px;
380 padding-left: 10px;
381 background-color: #eeeeee;
382 vertical-align: top;
315 383 }
316 /* hide add comment button when form is open */
384
385 .reply-thread-reply-button {
386 display: table-cell;
387 width: 100%;
388 height: 33px;
389 padding: 3px 8px;
390 margin-left: 8px;
391 background-color: #eeeeee;
392 }
393
394 .reply-thread-reply-button .cb-comment-add-button {
395 border-radius: 4px;
396 width: 100%;
397 padding: 6px 2px;
398 text-align: left;
399 cursor: text;
400 color: @grey3;
401 }
402 .reply-thread-reply-button .cb-comment-add-button:hover {
403 background-color: white;
404 color: @grey2;
405 }
406
407 .reply-thread-last {
408 display: table-cell;
409 width: 10px;
410 }
411
412 /* Hide reply box when it's a first element,
413 can happen when drafts are saved but not shown to specific user,
414 or there are outdated comments hidden
415 */
416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
417 display: none;
418 }
419
420 .reply-thread-container-wrapper.comment-outdated {
421 display: none
422 }
423
424 /* hide add comment button when form is open */
317 425 .comment-inline-form-open ~ .cb-comment-add-button {
318 426 display: none;
319 427 }
320 .comment-inline-form-open {
321 display: block;
322 }
323 /* hide add comment button when form but no comments */
324 .comment-inline-form:first-child + .cb-comment-add-button {
325 display: none;
326 }
327 /* hide add comment button when no comments or form */
328 .cb-comment-add-button:first-child {
329 display: none;
330 }
428
331 429 /* hide add comment button when only comment is being deleted */
332 430 .comment-deleting:first-child + .cb-comment-add-button {
333 431 display: none;
334 432 }
433
434 /* hide add comment button when form but no comments */
435 .comment-inline-form:first-child + .cb-comment-add-button {
436 display: none;
437 }
438
335 439 }
336 440
337
338 441 .show-outdated-comments {
339 442 display: inline;
340 443 color: @rcblue;
341 444 }
342 445
343 446 // Comment Form
344 447 div.comment-form {
345 448 margin-top: 20px;
346 449 }
347 450
348 451 .comment-form strong {
349 452 display: block;
350 453 margin-bottom: 15px;
351 454 }
352 455
353 456 .comment-form textarea {
354 457 width: 100%;
355 458 height: 100px;
356 459 font-family: @text-monospace;
357 460 }
358 461
359 462 form.comment-form {
360 463 margin-top: 10px;
361 464 margin-left: 10px;
362 465 }
363 466
364 467 .comment-inline-form .comment-block-ta,
365 468 .comment-form .comment-block-ta,
366 469 .comment-form .preview-box {
367 470 .border-radius(@border-radius);
368 471 .box-sizing(border-box);
369 472 background-color: white;
370 473 }
371 474
372 475 .comment-form-submit {
373 476 margin-top: 5px;
374 477 margin-left: 525px;
375 478 }
376 479
377 480 .file-comments {
378 481 display: none;
379 482 }
380 483
381 484 .comment-form .preview-box.unloaded,
382 485 .comment-inline-form .preview-box.unloaded {
383 486 height: 50px;
384 487 text-align: center;
385 488 padding: 20px;
386 489 background-color: white;
387 490 }
388 491
389 492 .comment-footer {
390 position: relative;
493 display: table;
391 494 width: 100%;
392 min-height: 42px;
495 height: 42px;
393 496
394 .status_box,
497 .comment-status-box,
395 498 .cancel-button {
396 float: left;
397 499 display: inline-block;
398 500 }
399 501
400 .status_box {
502 .comment-status-box {
401 503 margin-left: 10px;
402 504 }
403 505
404 506 .action-buttons {
405 float: left;
406 display: inline-block;
507 display: table-cell;
508 padding: 5px 0 5px 2px;
509 }
510
511 .toolbar-text {
512 height: 42px;
513 display: table-cell;
514 vertical-align: bottom;
515 font-size: 11px;
516 color: @grey4;
517 text-align: right;
518
519 a {
520 color: @grey4;
521 }
522
523 p {
524 padding: 0;
525 margin: 0;
526 }
407 527 }
408 528
409 529 .action-buttons-extra {
410 530 display: inline-block;
411 531 }
412 532 }
413 533
414 534 .comment-form {
415 535
416 536 .comment {
417 537 margin-left: 10px;
418 538 }
419 539
420 540 .comment-help {
421 541 color: @grey4;
422 542 padding: 5px 0 5px 0;
423 543 }
424 544
425 545 .comment-title {
426 546 padding: 5px 0 5px 0;
427 547 }
428 548
429 549 .comment-button {
430 550 display: inline-block;
431 551 }
432 552
433 553 .comment-button-input {
434 554 margin-right: 0;
435 555 }
436 556
437 .comment-footer {
438 margin-bottom: 50px;
439 margin-top: 10px;
440 }
441 557 }
442 558
443 559
444 560 .comment-form-login {
445 561 .comment-help {
446 562 padding: 0.7em; //same as the button
447 563 }
448 564
449 565 div.clearfix {
450 566 clear: both;
451 567 width: 100%;
452 568 display: block;
453 569 }
454 570 }
455 571
456 572 .comment-version-select {
457 573 margin: 0px;
458 574 border-radius: inherit;
459 575 border-color: @grey6;
460 576 height: 20px;
461 577 }
462 578
463 579 .comment-type {
464 580 margin: 0px;
465 581 border-radius: inherit;
466 582 border-color: @grey6;
467 583 }
468 584
469 585 .preview-box {
470 586 min-height: 105px;
471 587 margin-bottom: 15px;
472 588 background-color: white;
473 589 .border-radius(@border-radius);
474 590 .box-sizing(border-box);
475 591 }
476 592
477 593 .add-another-button {
478 594 margin-left: 10px;
479 595 margin-top: 10px;
480 596 margin-bottom: 10px;
481 597 }
482 598
483 599 .comment .buttons {
484 600 float: right;
485 601 margin: -1px 0px 0px 0px;
486 602 }
487 603
488 604 // Inline Comment Form
489 605 .injected_diff .comment-inline-form,
490 606 .comment-inline-form {
491 607 background-color: white;
492 margin-top: 10px;
493 margin-bottom: 20px;
608 margin-top: 4px;
609 margin-bottom: 10px;
494 610 }
495 611
496 612 .inline-form {
497 613 padding: 10px 7px;
498 614 }
499 615
500 616 .inline-form div {
501 617 max-width: 100%;
502 618 }
503 619
504 620 .overlay {
505 621 display: none;
506 622 position: absolute;
507 623 width: 100%;
508 624 text-align: center;
509 625 vertical-align: middle;
510 626 font-size: 16px;
511 627 background: none repeat scroll 0 0 white;
512 628
513 629 &.submitting {
514 630 display: block;
515 631 opacity: 0.5;
516 632 z-index: 100;
517 633 }
518 634 }
519 635 .comment-inline-form .overlay.submitting .overlay-text {
520 636 margin-top: 5%;
521 637 }
522 638
523 639 .comment-inline-form .clearfix,
524 640 .comment-form .clearfix {
525 641 .border-radius(@border-radius);
526 642 margin: 0px;
527 643 }
528 644
529 .comment-inline-form .comment-footer {
530 margin: 10px 0px 0px 0px;
531 }
532 645
533 646 .hide-inline-form-button {
534 647 margin-left: 5px;
535 648 }
536 649 .comment-button .hide-inline-form {
537 650 background: white;
538 651 }
539 652
540 653 .comment-area {
541 654 padding: 6px 8px;
542 655 border: 1px solid @grey5;
543 656 .border-radius(@border-radius);
544 657
545 658 .resolve-action {
546 659 padding: 1px 0px 0px 6px;
547 660 }
548 661
549 662 }
550 663
551 664 comment-area-text {
552 665 color: @grey3;
553 666 }
554 667
555 668 .comment-area-header {
556 669 height: 35px;
670 border-bottom: 1px solid @grey5;
557 671 }
558 672
559 673 .comment-area-header .nav-links {
560 674 display: flex;
561 675 flex-flow: row wrap;
562 676 -webkit-flex-flow: row wrap;
563 677 width: 100%;
678 border: none;
564 679 }
565 680
566 681 .comment-area-footer {
567 682 min-height: 30px;
568 683 }
569 684
570 685 .comment-footer .toolbar {
571 686
572 687 }
573 688
574 689 .comment-attachment-uploader {
575 690 border: 1px dashed white;
576 691 border-radius: @border-radius;
577 692 margin-top: -10px;
578 693 line-height: 30px;
579 694 &.dz-drag-hover {
580 695 border-color: @grey3;
581 696 }
582 697
583 698 .dz-error-message {
584 699 padding-top: 0;
585 700 }
586 701 }
587 702
588 703 .comment-attachment-text {
589 704 clear: both;
590 705 font-size: 11px;
591 706 color: #8F8F8F;
592 707 width: 100%;
593 708 .pick-attachment {
594 709 color: #8F8F8F;
595 710 }
596 711 .pick-attachment:hover {
597 712 color: @rcblue;
598 713 }
599 714 }
600 715
601 716 .nav-links {
602 717 padding: 0;
603 718 margin: 0;
604 719 list-style: none;
605 720 height: auto;
606 721 border-bottom: 1px solid @grey5;
607 722 }
608 723 .nav-links li {
609 724 display: inline-block;
610 725 list-style-type: none;
611 726 }
612 727
613 728 .nav-links li a.disabled {
614 729 cursor: not-allowed;
615 730 }
616 731
617 732 .nav-links li.active a {
618 733 border-bottom: 2px solid @rcblue;
619 734 color: #000;
620 735 font-weight: 600;
621 736 }
622 737 .nav-links li a {
623 738 display: inline-block;
624 739 padding: 0px 10px 5px 10px;
625 740 margin-bottom: -1px;
626 741 font-size: 14px;
627 742 line-height: 28px;
628 743 color: #8f8f8f;
629 744 border-bottom: 2px solid transparent;
630 745 }
631 746
632 .toolbar-text {
633 float: right;
634 font-size: 11px;
635 color: @grey4;
636 text-align: right;
637
638 a {
639 color: @grey4;
640 }
641 }
642
@@ -1,3239 +1,3243 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 @import 'tooltips';
30 30 @import 'sweetalert2';
31 31
32 32
33 33 //--- BASE ------------------//
34 34 .noscript-error {
35 35 top: 0;
36 36 left: 0;
37 37 width: 100%;
38 38 z-index: 101;
39 39 text-align: center;
40 40 font-size: 120%;
41 41 color: white;
42 42 background-color: @alert2;
43 43 padding: 5px 0 5px 0;
44 44 font-weight: @text-semibold-weight;
45 45 font-family: @text-semibold;
46 46 }
47 47
48 48 html {
49 49 display: table;
50 50 height: 100%;
51 51 width: 100%;
52 52 }
53 53
54 54 body {
55 55 display: table-cell;
56 56 width: 100%;
57 57 }
58 58
59 59 //--- LAYOUT ------------------//
60 60
61 61 .hidden{
62 62 display: none !important;
63 63 }
64 64
65 65 .box{
66 66 float: left;
67 67 width: 100%;
68 68 }
69 69
70 70 .browser-header {
71 71 clear: both;
72 72 }
73 73 .main {
74 74 clear: both;
75 75 padding:0 0 @pagepadding;
76 76 height: auto;
77 77
78 78 &:after { //clearfix
79 79 content:"";
80 80 clear:both;
81 81 width:100%;
82 82 display:block;
83 83 }
84 84 }
85 85
86 86 .flex-container {
87 87 display: flex;
88 88 justify-content: space-between;
89 89 }
90 90
91 91 .action-link{
92 92 margin-left: @padding;
93 93 padding-left: @padding;
94 94 border-left: @border-thickness solid @border-default-color;
95 95 }
96 96
97 97 .cursor-pointer {
98 98 cursor: pointer;
99 99 }
100 100
101 101 input + .action-link, .action-link.first{
102 102 border-left: none;
103 103 }
104 104
105 105 .link-disabled {
106 106 color: @grey4;
107 107 cursor: default;
108 108 }
109 109
110 110 .action-link.last{
111 111 margin-right: @padding;
112 112 padding-right: @padding;
113 113 }
114 114
115 115 .action-link.active,
116 116 .action-link.active a{
117 117 color: @grey4;
118 118 }
119 119
120 120 .action-link.disabled {
121 121 color: @grey4;
122 122 cursor: inherit;
123 123 }
124 124
125 125 .grey-link-action {
126 126 cursor: pointer;
127 127 &:hover {
128 128 color: @grey2;
129 129 }
130 130 color: @grey4;
131 131 }
132 132
133 133 .clipboard-action {
134 134 cursor: pointer;
135 135 margin-left: 5px;
136 136
137 137 &:not(.no-grey) {
138 138
139 139 &:hover {
140 140 color: @grey2;
141 141 }
142 142 color: @grey4;
143 143 }
144 144 }
145 145
146 146 ul.simple-list{
147 147 list-style: none;
148 148 margin: 0;
149 149 padding: 0;
150 150 }
151 151
152 152 .main-content {
153 153 padding-bottom: @pagepadding;
154 154 }
155 155
156 156 .wide-mode-wrapper {
157 157 max-width:4000px !important;
158 158 }
159 159
160 160 .wrapper {
161 161 position: relative;
162 162 max-width: @wrapper-maxwidth;
163 163 margin: 0 auto;
164 164 }
165 165
166 166 #content {
167 167 clear: both;
168 168 padding: 0 @contentpadding;
169 169 }
170 170
171 171 .advanced-settings-fields{
172 172 input{
173 173 margin-left: @textmargin;
174 174 margin-right: @padding/2;
175 175 }
176 176 }
177 177
178 178 .cs_files_title {
179 179 margin: @pagepadding 0 0;
180 180 }
181 181
182 182 input.inline[type="file"] {
183 183 display: inline;
184 184 }
185 185
186 186 .error_page {
187 187 margin: 10% auto;
188 188
189 189 h1 {
190 190 color: @grey2;
191 191 }
192 192
193 193 .alert {
194 194 margin: @padding 0;
195 195 }
196 196
197 197 .error-branding {
198 198 color: @grey4;
199 199 font-weight: @text-semibold-weight;
200 200 font-family: @text-semibold;
201 201 }
202 202
203 203 .error_message {
204 204 font-family: @text-regular;
205 205 }
206 206
207 207 .sidebar {
208 208 min-height: 275px;
209 209 margin: 0;
210 210 padding: 0 0 @sidebarpadding @sidebarpadding;
211 211 border: none;
212 212 }
213 213
214 214 .main-content {
215 215 position: relative;
216 216 margin: 0 @sidebarpadding @sidebarpadding;
217 217 padding: 0 0 0 @sidebarpadding;
218 218 border-left: @border-thickness solid @grey5;
219 219
220 220 @media (max-width:767px) {
221 221 clear: both;
222 222 width: 100%;
223 223 margin: 0;
224 224 border: none;
225 225 }
226 226 }
227 227
228 228 .inner-column {
229 229 float: left;
230 230 width: 29.75%;
231 231 min-height: 150px;
232 232 margin: @sidebarpadding 2% 0 0;
233 233 padding: 0 2% 0 0;
234 234 border-right: @border-thickness solid @grey5;
235 235
236 236 @media (max-width:767px) {
237 237 clear: both;
238 238 width: 100%;
239 239 border: none;
240 240 }
241 241
242 242 ul {
243 243 padding-left: 1.25em;
244 244 }
245 245
246 246 &:last-child {
247 247 margin: @sidebarpadding 0 0;
248 248 border: none;
249 249 }
250 250
251 251 h4 {
252 252 margin: 0 0 @padding;
253 253 font-weight: @text-semibold-weight;
254 254 font-family: @text-semibold;
255 255 }
256 256 }
257 257 }
258 258 .error-page-logo {
259 259 width: 130px;
260 260 height: 160px;
261 261 }
262 262
263 263 // HEADER
264 264 .header {
265 265
266 266 // TODO: johbo: Fix login pages, so that they work without a min-height
267 267 // for the header and then remove the min-height. I chose a smaller value
268 268 // intentionally here to avoid rendering issues in the main navigation.
269 269 min-height: 49px;
270 270 min-width: 1024px;
271 271
272 272 position: relative;
273 273 vertical-align: bottom;
274 274 padding: 0 @header-padding;
275 275 background-color: @grey1;
276 276 color: @grey5;
277 277
278 278 .title {
279 279 overflow: visible;
280 280 }
281 281
282 282 &:before,
283 283 &:after {
284 284 content: "";
285 285 clear: both;
286 286 width: 100%;
287 287 }
288 288
289 289 // TODO: johbo: Avoids breaking "Repositories" chooser
290 290 .select2-container .select2-choice .select2-arrow {
291 291 display: none;
292 292 }
293 293 }
294 294
295 295 #header-inner {
296 296 &.title {
297 297 margin: 0;
298 298 }
299 299 &:before,
300 300 &:after {
301 301 content: "";
302 302 clear: both;
303 303 }
304 304 }
305 305
306 306 // Gists
307 307 #files_data {
308 308 clear: both; //for firefox
309 309 padding-top: 10px;
310 310 }
311 311
312 312 #gistid {
313 313 margin-right: @padding;
314 314 }
315 315
316 316 // Global Settings Editor
317 317 .textarea.editor {
318 318 float: left;
319 319 position: relative;
320 320 max-width: @texteditor-width;
321 321
322 322 select {
323 323 position: absolute;
324 324 top:10px;
325 325 right:0;
326 326 }
327 327
328 328 .CodeMirror {
329 329 margin: 0;
330 330 }
331 331
332 332 .help-block {
333 333 margin: 0 0 @padding;
334 334 padding:.5em;
335 335 background-color: @grey6;
336 336 &.pre-formatting {
337 337 white-space: pre;
338 338 }
339 339 }
340 340 }
341 341
342 342 ul.auth_plugins {
343 343 margin: @padding 0 @padding @legend-width;
344 344 padding: 0;
345 345
346 346 li {
347 347 margin-bottom: @padding;
348 348 line-height: 1em;
349 349 list-style-type: none;
350 350
351 351 .auth_buttons .btn {
352 352 margin-right: @padding;
353 353 }
354 354
355 355 }
356 356 }
357 357
358 358
359 359 // My Account PR list
360 360
361 361 #show_closed {
362 362 margin: 0 1em 0 0;
363 363 }
364 364
365 365 #pull_request_list_table {
366 366 .closed {
367 367 background-color: @grey6;
368 368 }
369 369
370 370 .state-creating,
371 371 .state-updating,
372 372 .state-merging
373 373 {
374 374 background-color: @grey6;
375 375 }
376 376
377 377 .log-container .truncate {
378 378 height: 2.75em;
379 379 white-space: pre-line;
380 380 }
381 381 table.rctable .user {
382 382 padding-left: 0;
383 383 }
384 384 .td-status {
385 385 padding: 0 0px 0px 10px;
386 386 width: 15px;
387 387 }
388 388 table.rctable {
389 389 td.td-description,
390 390 .rc-user {
391 391 min-width: auto;
392 392 }
393 393 }
394 394 }
395 395
396 396 // Pull Requests
397 397
398 398 .pullrequests_section_head {
399 399 display: block;
400 400 clear: both;
401 401 margin: @padding 0;
402 402 font-weight: @text-bold-weight;
403 403 font-family: @text-bold;
404 404 }
405 405
406 406 .pr-commit-flow {
407 407 position: relative;
408 408 font-weight: 600;
409 409
410 410 .tag {
411 411 display: inline-block;
412 412 margin: 0 1em .5em 0;
413 413 }
414 414
415 415 .clone-url {
416 416 display: inline-block;
417 417 margin: 0 0 .5em 0;
418 418 padding: 0;
419 419 line-height: 1.2em;
420 420 }
421 421 }
422 422
423 423 .pr-mergeinfo {
424 424 min-width: 95% !important;
425 425 padding: 0 !important;
426 426 border: 0;
427 427 }
428 428 .pr-mergeinfo-copy {
429 429 padding: 0 0;
430 430 }
431 431
432 432 .pr-pullinfo {
433 433 min-width: 95% !important;
434 434 padding: 0 !important;
435 435 border: 0;
436 436 }
437 437 .pr-pullinfo-copy {
438 438 padding: 0 0;
439 439 }
440 440
441 441 .pr-title-input {
442 442 width: 100%;
443 443 font-size: 18px;
444 444 margin: 0 0 4px 0;
445 445 padding: 0;
446 446 line-height: 1.7em;
447 447 color: @text-color;
448 448 letter-spacing: .02em;
449 449 font-weight: @text-bold-weight;
450 450 font-family: @text-bold;
451 451
452 452 &:hover {
453 453 box-shadow: none;
454 454 }
455 455 }
456 456
457 457 #pr-title {
458 458 input {
459 459 border: 1px transparent;
460 460 color: black;
461 461 opacity: 1;
462 462 background: #fff;
463 463 font-size: 18px;
464 464 }
465 465 }
466 466
467 467 .pr-title-closed-tag {
468 468 font-size: 16px;
469 469 }
470 470
471 471 #pr-desc {
472 472 padding: 10px 0;
473 473
474 474 .markdown-block {
475 475 padding: 0;
476 476 margin-bottom: -30px;
477 477 }
478 478 }
479 479
480 480 #pullrequest_title {
481 481 width: 100%;
482 482 box-sizing: border-box;
483 483 }
484 484
485 485 #pr_open_message {
486 486 border: @border-thickness solid #fff;
487 487 border-radius: @border-radius;
488 488 text-align: left;
489 489 overflow: hidden;
490 490 white-space: pre-line;
491 491 padding-top: 5px
492 492 }
493 493
494 494 #add_reviewer {
495 495 padding-top: 10px;
496 496 }
497 497
498 498 #add_reviewer_input,
499 499 #add_observer_input {
500 500 padding-top: 10px
501 501 }
502 502
503 503 .pr-details-title-author-pref {
504 504 padding-right: 10px
505 505 }
506 506
507 507 .label-pr-detail {
508 508 display: table-cell;
509 509 width: 120px;
510 510 padding-top: 7.5px;
511 511 padding-bottom: 7.5px;
512 512 padding-right: 7.5px;
513 513 }
514 514
515 515 .source-details ul {
516 516 padding: 10px 16px;
517 517 }
518 518
519 519 .source-details-action {
520 520 color: @grey4;
521 521 font-size: 11px
522 522 }
523 523
524 524 .pr-submit-button {
525 525 float: right;
526 526 margin: 0 0 0 5px;
527 527 }
528 528
529 529 .pr-spacing-container {
530 530 padding: 20px;
531 531 clear: both
532 532 }
533 533
534 534 #pr-description-input {
535 535 margin-bottom: 0;
536 536 }
537 537
538 538 .pr-description-label {
539 539 vertical-align: top;
540 540 }
541 541
542 542 #open_edit_pullrequest {
543 543 padding: 0;
544 544 }
545 545
546 546 #close_edit_pullrequest {
547 547
548 548 }
549 549
550 550 #delete_pullrequest {
551 551 clear: inherit;
552 552
553 553 form {
554 554 display: inline;
555 555 }
556 556
557 557 }
558 558
559 559 .perms_section_head {
560 560 min-width: 625px;
561 561
562 562 h2 {
563 563 margin-bottom: 0;
564 564 }
565 565
566 566 .label-checkbox {
567 567 float: left;
568 568 }
569 569
570 570 &.field {
571 571 margin: @space 0 @padding;
572 572 }
573 573
574 574 &:first-child.field {
575 575 margin-top: 0;
576 576
577 577 .label {
578 578 margin-top: 0;
579 579 padding-top: 0;
580 580 }
581 581
582 582 .radios {
583 583 padding-top: 0;
584 584 }
585 585 }
586 586
587 587 .radios {
588 588 position: relative;
589 589 width: 505px;
590 590 }
591 591 }
592 592
593 593 //--- MODULES ------------------//
594 594
595 595
596 596 // Server Announcement
597 597 #server-announcement {
598 598 width: 95%;
599 599 margin: @padding auto;
600 600 padding: @padding;
601 601 border-width: 2px;
602 602 border-style: solid;
603 603 .border-radius(2px);
604 604 font-weight: @text-bold-weight;
605 605 font-family: @text-bold;
606 606
607 607 &.info { border-color: @alert4; background-color: @alert4-inner; }
608 608 &.warning { border-color: @alert3; background-color: @alert3-inner; }
609 609 &.error { border-color: @alert2; background-color: @alert2-inner; }
610 610 &.success { border-color: @alert1; background-color: @alert1-inner; }
611 611 &.neutral { border-color: @grey3; background-color: @grey6; }
612 612 }
613 613
614 614 // Fixed Sidebar Column
615 615 .sidebar-col-wrapper {
616 616 padding-left: @sidebar-all-width;
617 617
618 618 .sidebar {
619 619 width: @sidebar-width;
620 620 margin-left: -@sidebar-all-width;
621 621 }
622 622 }
623 623
624 624 .sidebar-col-wrapper.scw-small {
625 625 padding-left: @sidebar-small-all-width;
626 626
627 627 .sidebar {
628 628 width: @sidebar-small-width;
629 629 margin-left: -@sidebar-small-all-width;
630 630 }
631 631 }
632 632
633 633
634 634 // FOOTER
635 635 #footer {
636 636 padding: 0;
637 637 text-align: center;
638 638 vertical-align: middle;
639 639 color: @grey2;
640 640 font-size: 11px;
641 641
642 642 p {
643 643 margin: 0;
644 644 padding: 1em;
645 645 line-height: 1em;
646 646 }
647 647
648 648 .server-instance { //server instance
649 649 display: none;
650 650 }
651 651
652 652 .title {
653 653 float: none;
654 654 margin: 0 auto;
655 655 }
656 656 }
657 657
658 658 button.close {
659 659 padding: 0;
660 660 cursor: pointer;
661 661 background: transparent;
662 662 border: 0;
663 663 .box-shadow(none);
664 664 -webkit-appearance: none;
665 665 }
666 666
667 667 .close {
668 668 float: right;
669 669 font-size: 21px;
670 670 font-family: @text-bootstrap;
671 671 line-height: 1em;
672 672 font-weight: bold;
673 673 color: @grey2;
674 674
675 675 &:hover,
676 676 &:focus {
677 677 color: @grey1;
678 678 text-decoration: none;
679 679 cursor: pointer;
680 680 }
681 681 }
682 682
683 683 // GRID
684 684 .sorting,
685 685 .sorting_desc,
686 686 .sorting_asc {
687 687 cursor: pointer;
688 688 }
689 689 .sorting_desc:after {
690 690 content: "\00A0\25B2";
691 691 font-size: .75em;
692 692 }
693 693 .sorting_asc:after {
694 694 content: "\00A0\25BC";
695 695 font-size: .68em;
696 696 }
697 697
698 698
699 699 .user_auth_tokens {
700 700
701 701 &.truncate {
702 702 white-space: nowrap;
703 703 overflow: hidden;
704 704 text-overflow: ellipsis;
705 705 }
706 706
707 707 .fields .field .input {
708 708 margin: 0;
709 709 }
710 710
711 711 input#description {
712 712 width: 100px;
713 713 margin: 0;
714 714 }
715 715
716 716 .drop-menu {
717 717 // TODO: johbo: Remove this, should work out of the box when
718 718 // having multiple inputs inline
719 719 margin: 0 0 0 5px;
720 720 }
721 721 }
722 722 #user_list_table {
723 723 .closed {
724 724 background-color: @grey6;
725 725 }
726 726 }
727 727
728 728
729 729 input, textarea {
730 730 &.disabled {
731 731 opacity: .5;
732 732 }
733 733
734 734 &:hover {
735 735 border-color: @grey3;
736 736 box-shadow: @button-shadow;
737 737 }
738 738
739 739 &:focus {
740 740 border-color: @rcblue;
741 741 box-shadow: @button-shadow;
742 742 }
743 743 }
744 744
745 745 // remove extra padding in firefox
746 746 input::-moz-focus-inner { border:0; padding:0 }
747 747
748 748 .adjacent input {
749 749 margin-bottom: @padding;
750 750 }
751 751
752 752 .permissions_boxes {
753 753 display: block;
754 754 }
755 755
756 756 //FORMS
757 757
758 758 .medium-inline,
759 759 input#description.medium-inline {
760 760 display: inline;
761 761 width: @medium-inline-input-width;
762 762 min-width: 100px;
763 763 }
764 764
765 765 select {
766 766 //reset
767 767 -webkit-appearance: none;
768 768 -moz-appearance: none;
769 769
770 770 display: inline-block;
771 771 height: 28px;
772 772 width: auto;
773 773 margin: 0 @padding @padding 0;
774 774 padding: 0 18px 0 8px;
775 775 line-height:1em;
776 776 font-size: @basefontsize;
777 777 border: @border-thickness solid @grey5;
778 778 border-radius: @border-radius;
779 779 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
780 780 color: @grey4;
781 781 box-shadow: @button-shadow;
782 782
783 783 &:after {
784 784 content: "\00A0\25BE";
785 785 }
786 786
787 787 &:focus, &:hover {
788 788 outline: none;
789 789 border-color: @grey4;
790 790 color: @rcdarkblue;
791 791 }
792 792 }
793 793
794 794 option {
795 795 &:focus {
796 796 outline: none;
797 797 }
798 798 }
799 799
800 800 input,
801 801 textarea {
802 802 padding: @input-padding;
803 803 border: @input-border-thickness solid @border-highlight-color;
804 804 .border-radius (@border-radius);
805 805 font-family: @text-light;
806 806 font-size: @basefontsize;
807 807
808 808 &.input-sm {
809 809 padding: 5px;
810 810 }
811 811
812 812 &#description {
813 813 min-width: @input-description-minwidth;
814 814 min-height: 1em;
815 815 padding: 10px;
816 816 }
817 817 }
818 818
819 819 .field-sm {
820 820 input,
821 821 textarea {
822 822 padding: 5px;
823 823 }
824 824 }
825 825
826 826 textarea {
827 827 display: block;
828 828 clear: both;
829 829 width: 100%;
830 830 min-height: 100px;
831 831 margin-bottom: @padding;
832 832 .box-sizing(border-box);
833 833 overflow: auto;
834 834 }
835 835
836 836 label {
837 837 font-family: @text-light;
838 838 }
839 839
840 840 // GRAVATARS
841 841 // centers gravatar on username to the right
842 842
843 843 .gravatar {
844 844 display: inline;
845 845 min-width: 16px;
846 846 min-height: 16px;
847 847 margin: -5px 0;
848 848 padding: 0;
849 849 line-height: 1em;
850 850 box-sizing: content-box;
851 851 border-radius: 50%;
852 852
853 853 &.gravatar-large {
854 854 margin: -0.5em .25em -0.5em 0;
855 855 }
856 856
857 857 & + .user {
858 858 display: inline;
859 859 margin: 0;
860 860 padding: 0 0 0 .17em;
861 861 line-height: 1em;
862 862 }
863 863
864 864 & + .no-margin {
865 865 margin: 0
866 866 }
867 867
868 868 }
869 869
870 870 .user-inline-data {
871 871 display: inline-block;
872 872 float: left;
873 873 padding-left: .5em;
874 874 line-height: 1.3em;
875 875 }
876 876
877 877 .rc-user { // gravatar + user wrapper
878 878 float: left;
879 879 position: relative;
880 880 min-width: 100px;
881 881 max-width: 200px;
882 882 min-height: (@gravatar-size + @border-thickness * 2); // account for border
883 883 display: block;
884 884 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
885 885
886 886
887 887 .gravatar {
888 888 display: block;
889 889 position: absolute;
890 890 top: 0;
891 891 left: 0;
892 892 min-width: @gravatar-size;
893 893 min-height: @gravatar-size;
894 894 margin: 0;
895 895 }
896 896
897 897 .user {
898 898 display: block;
899 899 max-width: 175px;
900 900 padding-top: 2px;
901 901 overflow: hidden;
902 902 text-overflow: ellipsis;
903 903 }
904 904 }
905 905
906 906 .gist-gravatar,
907 907 .journal_container {
908 908 .gravatar-large {
909 909 margin: 0 .5em -10px 0;
910 910 }
911 911 }
912 912
913 913 .gist-type-fields {
914 914 line-height: 30px;
915 915 height: 30px;
916 916
917 917 .gist-type-fields-wrapper {
918 918 vertical-align: middle;
919 919 display: inline-block;
920 920 line-height: 25px;
921 921 }
922 922 }
923 923
924 924 // ADMIN SETTINGS
925 925
926 926 // Tag Patterns
927 927 .tag_patterns {
928 928 .tag_input {
929 929 margin-bottom: @padding;
930 930 }
931 931 }
932 932
933 933 .locked_input {
934 934 position: relative;
935 935
936 936 input {
937 937 display: inline;
938 938 margin: 3px 5px 0px 0px;
939 939 }
940 940
941 941 br {
942 942 display: none;
943 943 }
944 944
945 945 .error-message {
946 946 float: left;
947 947 width: 100%;
948 948 }
949 949
950 950 .lock_input_button {
951 951 display: inline;
952 952 }
953 953
954 954 .help-block {
955 955 clear: both;
956 956 }
957 957 }
958 958
959 959 // Notifications
960 960
961 961 .notifications_buttons {
962 962 margin: 0 0 @space 0;
963 963 padding: 0;
964 964
965 965 .btn {
966 966 display: inline-block;
967 967 }
968 968 }
969 969
970 970 .notification-list {
971 971
972 972 div {
973 973 vertical-align: middle;
974 974 }
975 975
976 976 .container {
977 977 display: block;
978 978 margin: 0 0 @padding 0;
979 979 }
980 980
981 981 .delete-notifications {
982 982 margin-left: @padding;
983 983 text-align: right;
984 984 cursor: pointer;
985 985 }
986 986
987 987 .read-notifications {
988 988 margin-left: @padding/2;
989 989 text-align: right;
990 990 width: 35px;
991 991 cursor: pointer;
992 992 }
993 993
994 994 .icon-minus-sign {
995 995 color: @alert2;
996 996 }
997 997
998 998 .icon-ok-sign {
999 999 color: @alert1;
1000 1000 }
1001 1001 }
1002 1002
1003 1003 .user_settings {
1004 1004 float: left;
1005 1005 clear: both;
1006 1006 display: block;
1007 1007 width: 100%;
1008 1008
1009 1009 .gravatar_box {
1010 1010 margin-bottom: @padding;
1011 1011
1012 1012 &:after {
1013 1013 content: " ";
1014 1014 clear: both;
1015 1015 width: 100%;
1016 1016 }
1017 1017 }
1018 1018
1019 1019 .fields .field {
1020 1020 clear: both;
1021 1021 }
1022 1022 }
1023 1023
1024 1024 .advanced_settings {
1025 1025 margin-bottom: @space;
1026 1026
1027 1027 .help-block {
1028 1028 margin-left: 0;
1029 1029 }
1030 1030
1031 1031 button + .help-block {
1032 1032 margin-top: @padding;
1033 1033 }
1034 1034 }
1035 1035
1036 1036 // admin settings radio buttons and labels
1037 1037 .label-2 {
1038 1038 float: left;
1039 1039 width: @label2-width;
1040 1040
1041 1041 label {
1042 1042 color: @grey1;
1043 1043 }
1044 1044 }
1045 1045 .checkboxes {
1046 1046 float: left;
1047 1047 width: @checkboxes-width;
1048 1048 margin-bottom: @padding;
1049 1049
1050 1050 .checkbox {
1051 1051 width: 100%;
1052 1052
1053 1053 label {
1054 1054 margin: 0;
1055 1055 padding: 0;
1056 1056 }
1057 1057 }
1058 1058
1059 1059 .checkbox + .checkbox {
1060 1060 display: inline-block;
1061 1061 }
1062 1062
1063 1063 label {
1064 1064 margin-right: 1em;
1065 1065 }
1066 1066 }
1067 1067
1068 1068 // CHANGELOG
1069 1069 .container_header {
1070 1070 float: left;
1071 1071 display: block;
1072 1072 width: 100%;
1073 1073 margin: @padding 0 @padding;
1074 1074
1075 1075 #filter_changelog {
1076 1076 float: left;
1077 1077 margin-right: @padding;
1078 1078 }
1079 1079
1080 1080 .breadcrumbs_light {
1081 1081 display: inline-block;
1082 1082 }
1083 1083 }
1084 1084
1085 1085 .info_box {
1086 1086 float: right;
1087 1087 }
1088 1088
1089 1089
1090 1090
1091 1091 #graph_content{
1092 1092
1093 1093 // adjust for table headers so that graph renders properly
1094 1094 // #graph_nodes padding - table cell padding
1095 1095 padding-top: (@space - (@basefontsize * 2.4));
1096 1096
1097 1097 &.graph_full_width {
1098 1098 width: 100%;
1099 1099 max-width: 100%;
1100 1100 }
1101 1101 }
1102 1102
1103 1103 #graph {
1104 1104
1105 1105 .pagination-left {
1106 1106 float: left;
1107 1107 clear: both;
1108 1108 }
1109 1109
1110 1110 .log-container {
1111 1111 max-width: 345px;
1112 1112
1113 1113 .message{
1114 1114 max-width: 340px;
1115 1115 }
1116 1116 }
1117 1117
1118 1118 .graph-col-wrapper {
1119 1119
1120 1120 #graph_nodes {
1121 1121 width: 100px;
1122 1122 position: absolute;
1123 1123 left: 70px;
1124 1124 z-index: -1;
1125 1125 }
1126 1126 }
1127 1127
1128 1128 .load-more-commits {
1129 1129 text-align: center;
1130 1130 }
1131 1131 .load-more-commits:hover {
1132 1132 background-color: @grey7;
1133 1133 }
1134 1134 .load-more-commits {
1135 1135 a {
1136 1136 display: block;
1137 1137 }
1138 1138 }
1139 1139 }
1140 1140
1141 1141 .obsolete-toggle {
1142 1142 line-height: 30px;
1143 1143 margin-left: -15px;
1144 1144 }
1145 1145
1146 1146 #rev_range_container, #rev_range_clear, #rev_range_more {
1147 1147 margin-top: -5px;
1148 1148 margin-bottom: -5px;
1149 1149 }
1150 1150
1151 1151 #filter_changelog {
1152 1152 float: left;
1153 1153 }
1154 1154
1155 1155
1156 1156 //--- THEME ------------------//
1157 1157
1158 1158 #logo {
1159 1159 float: left;
1160 1160 margin: 9px 0 0 0;
1161 1161
1162 1162 .header {
1163 1163 background-color: transparent;
1164 1164 }
1165 1165
1166 1166 a {
1167 1167 display: inline-block;
1168 1168 }
1169 1169
1170 1170 img {
1171 1171 height:30px;
1172 1172 }
1173 1173 }
1174 1174
1175 1175 .logo-wrapper {
1176 1176 float:left;
1177 1177 }
1178 1178
1179 1179 .branding {
1180 1180 float: left;
1181 1181 padding: 9px 2px;
1182 1182 line-height: 1em;
1183 1183 font-size: @navigation-fontsize;
1184 1184
1185 1185 a {
1186 1186 color: @grey5
1187 1187 }
1188 1188
1189 1189 // 1024px or smaller
1190 1190 @media screen and (max-width: 1180px) {
1191 1191 display: none;
1192 1192 }
1193 1193
1194 1194 }
1195 1195
1196 1196 img {
1197 1197 border: none;
1198 1198 outline: none;
1199 1199 }
1200 1200 user-profile-header
1201 1201 label {
1202 1202
1203 1203 input[type="checkbox"] {
1204 1204 margin-right: 1em;
1205 1205 }
1206 1206 input[type="radio"] {
1207 1207 margin-right: 1em;
1208 1208 }
1209 1209 }
1210 1210
1211 1211 .review-status {
1212 1212 &.under_review {
1213 1213 color: @alert3;
1214 1214 }
1215 1215 &.approved {
1216 1216 color: @alert1;
1217 1217 }
1218 1218 &.rejected,
1219 1219 &.forced_closed{
1220 1220 color: @alert2;
1221 1221 }
1222 1222 &.not_reviewed {
1223 1223 color: @grey5;
1224 1224 }
1225 1225 }
1226 1226
1227 1227 .review-status-under_review {
1228 1228 color: @alert3;
1229 1229 }
1230 1230 .status-tag-under_review {
1231 1231 border-color: @alert3;
1232 1232 }
1233 1233
1234 1234 .review-status-approved {
1235 1235 color: @alert1;
1236 1236 }
1237 1237 .status-tag-approved {
1238 1238 border-color: @alert1;
1239 1239 }
1240 1240
1241 1241 .review-status-rejected,
1242 1242 .review-status-forced_closed {
1243 1243 color: @alert2;
1244 1244 }
1245 1245 .status-tag-rejected,
1246 1246 .status-tag-forced_closed {
1247 1247 border-color: @alert2;
1248 1248 }
1249 1249
1250 1250 .review-status-not_reviewed {
1251 1251 color: @grey5;
1252 1252 }
1253 1253 .status-tag-not_reviewed {
1254 1254 border-color: @grey5;
1255 1255 }
1256 1256
1257 1257 .test_pattern_preview {
1258 1258 margin: @space 0;
1259 1259
1260 1260 p {
1261 1261 margin-bottom: 0;
1262 1262 border-bottom: @border-thickness solid @border-default-color;
1263 1263 color: @grey3;
1264 1264 }
1265 1265
1266 1266 .btn {
1267 1267 margin-bottom: @padding;
1268 1268 }
1269 1269 }
1270 1270 #test_pattern_result {
1271 1271 display: none;
1272 1272 &:extend(pre);
1273 1273 padding: .9em;
1274 1274 color: @grey3;
1275 1275 background-color: @grey7;
1276 1276 border-right: @border-thickness solid @border-default-color;
1277 1277 border-bottom: @border-thickness solid @border-default-color;
1278 1278 border-left: @border-thickness solid @border-default-color;
1279 1279 }
1280 1280
1281 1281 #repo_vcs_settings {
1282 1282 #inherit_overlay_vcs_default {
1283 1283 display: none;
1284 1284 }
1285 1285 #inherit_overlay_vcs_custom {
1286 1286 display: custom;
1287 1287 }
1288 1288 &.inherited {
1289 1289 #inherit_overlay_vcs_default {
1290 1290 display: block;
1291 1291 }
1292 1292 #inherit_overlay_vcs_custom {
1293 1293 display: none;
1294 1294 }
1295 1295 }
1296 1296 }
1297 1297
1298 1298 .issue-tracker-link {
1299 1299 color: @rcblue;
1300 1300 }
1301 1301
1302 1302 // Issue Tracker Table Show/Hide
1303 1303 #repo_issue_tracker {
1304 1304 #inherit_overlay {
1305 1305 display: none;
1306 1306 }
1307 1307 #custom_overlay {
1308 1308 display: custom;
1309 1309 }
1310 1310 &.inherited {
1311 1311 #inherit_overlay {
1312 1312 display: block;
1313 1313 }
1314 1314 #custom_overlay {
1315 1315 display: none;
1316 1316 }
1317 1317 }
1318 1318 }
1319 1319 table.issuetracker {
1320 1320 &.readonly {
1321 1321 tr, td {
1322 1322 color: @grey3;
1323 1323 }
1324 1324 }
1325 1325 .edit {
1326 1326 display: none;
1327 1327 }
1328 1328 .editopen {
1329 1329 .edit {
1330 1330 display: inline;
1331 1331 }
1332 1332 .entry {
1333 1333 display: none;
1334 1334 }
1335 1335 }
1336 1336 tr td.td-action {
1337 1337 min-width: 117px;
1338 1338 }
1339 1339 td input {
1340 1340 max-width: none;
1341 1341 min-width: 30px;
1342 1342 width: 80%;
1343 1343 }
1344 1344 .issuetracker_pref input {
1345 1345 width: 40%;
1346 1346 }
1347 1347 input.edit_issuetracker_update {
1348 1348 margin-right: 0;
1349 1349 width: auto;
1350 1350 }
1351 1351 }
1352 1352
1353 1353 table.integrations {
1354 1354 .td-icon {
1355 1355 width: 20px;
1356 1356 .integration-icon {
1357 1357 height: 20px;
1358 1358 width: 20px;
1359 1359 }
1360 1360 }
1361 1361 }
1362 1362
1363 1363 .integrations {
1364 1364 a.integration-box {
1365 1365 color: @text-color;
1366 1366 &:hover {
1367 1367 .panel {
1368 1368 background: #fbfbfb;
1369 1369 }
1370 1370 }
1371 1371 .integration-icon {
1372 1372 width: 30px;
1373 1373 height: 30px;
1374 1374 margin-right: 20px;
1375 1375 float: left;
1376 1376 }
1377 1377
1378 1378 .panel-body {
1379 1379 padding: 10px;
1380 1380 }
1381 1381 .panel {
1382 1382 margin-bottom: 10px;
1383 1383 }
1384 1384 h2 {
1385 1385 display: inline-block;
1386 1386 margin: 0;
1387 1387 min-width: 140px;
1388 1388 }
1389 1389 }
1390 1390 a.integration-box.dummy-integration {
1391 1391 color: @grey4
1392 1392 }
1393 1393 }
1394 1394
1395 1395 //Permissions Settings
1396 1396 #add_perm {
1397 1397 margin: 0 0 @padding;
1398 1398 cursor: pointer;
1399 1399 }
1400 1400
1401 1401 .perm_ac {
1402 1402 input {
1403 1403 width: 95%;
1404 1404 }
1405 1405 }
1406 1406
1407 1407 .autocomplete-suggestions {
1408 1408 width: auto !important; // overrides autocomplete.js
1409 1409 min-width: 278px;
1410 1410 margin: 0;
1411 1411 border: @border-thickness solid @grey5;
1412 1412 border-radius: @border-radius;
1413 1413 color: @grey2;
1414 1414 background-color: white;
1415 1415 }
1416 1416
1417 1417 .autocomplete-qfilter-suggestions {
1418 1418 width: auto !important; // overrides autocomplete.js
1419 1419 max-height: 100% !important;
1420 1420 min-width: 376px;
1421 1421 margin: 0;
1422 1422 border: @border-thickness solid @grey5;
1423 1423 color: @grey2;
1424 1424 background-color: white;
1425 1425 }
1426 1426
1427 1427 .autocomplete-selected {
1428 1428 background: #F0F0F0;
1429 1429 }
1430 1430
1431 1431 .ac-container-wrap {
1432 1432 margin: 0;
1433 1433 padding: 8px;
1434 1434 border-bottom: @border-thickness solid @grey5;
1435 1435 list-style-type: none;
1436 1436 cursor: pointer;
1437 1437
1438 1438 &:hover {
1439 1439 background-color: @grey7;
1440 1440 }
1441 1441
1442 1442 img {
1443 1443 height: @gravatar-size;
1444 1444 width: @gravatar-size;
1445 1445 margin-right: 1em;
1446 1446 }
1447 1447
1448 1448 strong {
1449 1449 font-weight: normal;
1450 1450 }
1451 1451 }
1452 1452
1453 1453 // Settings Dropdown
1454 1454 .user-menu .container {
1455 1455 padding: 0 4px;
1456 1456 margin: 0;
1457 1457 }
1458 1458
1459 1459 .user-menu .gravatar {
1460 1460 cursor: pointer;
1461 1461 }
1462 1462
1463 1463 .codeblock {
1464 1464 margin-bottom: @padding;
1465 1465 clear: both;
1466 1466
1467 1467 .stats {
1468 1468 overflow: hidden;
1469 1469 }
1470 1470
1471 1471 .message{
1472 1472 textarea{
1473 1473 margin: 0;
1474 1474 }
1475 1475 }
1476 1476
1477 1477 .code-header {
1478 1478 .stats {
1479 1479 line-height: 2em;
1480 1480
1481 1481 .revision_id {
1482 1482 margin-left: 0;
1483 1483 }
1484 1484 .buttons {
1485 1485 padding-right: 0;
1486 1486 }
1487 1487 }
1488 1488
1489 1489 .item{
1490 1490 margin-right: 0.5em;
1491 1491 }
1492 1492 }
1493 1493
1494 1494 #editor_container {
1495 1495 position: relative;
1496 1496 margin: @padding 10px;
1497 1497 }
1498 1498 }
1499 1499
1500 1500 #file_history_container {
1501 1501 display: none;
1502 1502 }
1503 1503
1504 1504 .file-history-inner {
1505 1505 margin-bottom: 10px;
1506 1506 }
1507 1507
1508 1508 // Pull Requests
1509 1509 .summary-details {
1510 1510 width: 100%;
1511 1511 }
1512 1512 .pr-summary {
1513 1513 border-bottom: @border-thickness solid @grey5;
1514 1514 margin-bottom: @space;
1515 1515 }
1516 1516
1517 1517 .reviewers {
1518 1518 width: 98%;
1519 1519 }
1520 1520
1521 1521 .reviewers ul li {
1522 1522 position: relative;
1523 1523 width: 100%;
1524 1524 padding-bottom: 8px;
1525 1525 list-style-type: none;
1526 1526 }
1527 1527
1528 1528 .reviewer_entry {
1529 1529 min-height: 55px;
1530 1530 }
1531 1531
1532 1532 .reviewer_reason {
1533 1533 padding-left: 20px;
1534 1534 line-height: 1.5em;
1535 1535 }
1536 1536 .reviewer_status {
1537 1537 display: inline-block;
1538 1538 width: 20px;
1539 1539 min-width: 20px;
1540 1540 height: 1.2em;
1541 1541 line-height: 1em;
1542 1542 }
1543 1543
1544 1544 .reviewer_name {
1545 1545 display: inline-block;
1546 1546 max-width: 83%;
1547 1547 padding-right: 20px;
1548 1548 vertical-align: middle;
1549 1549 line-height: 1;
1550 1550
1551 1551 .rc-user {
1552 1552 min-width: 0;
1553 1553 margin: -2px 1em 0 0;
1554 1554 }
1555 1555
1556 1556 .reviewer {
1557 1557 float: left;
1558 1558 }
1559 1559 }
1560 1560
1561 1561 .reviewer_member_mandatory {
1562 1562 width: 16px;
1563 1563 font-size: 11px;
1564 1564 margin: 0;
1565 1565 padding: 0;
1566 1566 color: black;
1567 1567 opacity: 0.4;
1568 1568 }
1569 1569
1570 1570 .reviewer_member_mandatory_remove,
1571 1571 .reviewer_member_remove {
1572 1572 width: 16px;
1573 1573 padding: 0;
1574 1574 color: black;
1575 1575 cursor: pointer;
1576 1576 }
1577 1577
1578 1578 .reviewer_member_mandatory_remove {
1579 1579 color: @grey4;
1580 1580 }
1581 1581
1582 1582 .reviewer_member_status {
1583 1583 margin-top: 5px;
1584 1584 }
1585 1585 .pr-summary #summary{
1586 1586 width: 100%;
1587 1587 }
1588 1588 .pr-summary .action_button:hover {
1589 1589 border: 0;
1590 1590 cursor: pointer;
1591 1591 }
1592 1592 .pr-details-title {
1593 1593 height: 20px;
1594 1594 line-height: 16px;
1595 1595
1596 1596 padding-bottom: 4px;
1597 1597 border-bottom: @border-thickness solid @grey5;
1598 1598
1599 1599 .action_button.disabled {
1600 1600 color: @grey4;
1601 1601 cursor: inherit;
1602 1602 }
1603 1603 .action_button {
1604 1604 color: @rcblue;
1605 1605 }
1606 1606 }
1607 1607 .pr-details-content {
1608 1608 margin-top: @textmargin - 5;
1609 1609 margin-bottom: @textmargin - 5;
1610 1610 }
1611 1611
1612 1612 .pr-reviewer-rules {
1613 1613 padding: 10px 0px 20px 0px;
1614 1614 }
1615 1615
1616 1616 .todo-resolved {
1617 1617 text-decoration: line-through;
1618 1618 }
1619 1619
1620 1620 .todo-table, .comments-table {
1621 1621 width: 100%;
1622 1622
1623 1623 td {
1624 1624 padding: 5px 0px;
1625 1625 }
1626 1626
1627 1627 .td-todo-number {
1628 1628 text-align: left;
1629 1629 white-space: nowrap;
1630 1630 width: 1%;
1631 1631 padding-right: 2px;
1632 1632 }
1633 1633
1634 1634 .td-todo-gravatar {
1635 1635 width: 5%;
1636 1636
1637 1637 img {
1638 1638 margin: -3px 0;
1639 1639 }
1640 1640 }
1641 1641
1642 1642 }
1643 1643
1644 1644 .todo-comment-text-wrapper {
1645 1645 display: inline-grid;
1646 1646 }
1647 1647
1648 1648 .todo-comment-text {
1649 1649 margin-left: 5px;
1650 1650 white-space: nowrap;
1651 1651 overflow: hidden;
1652 1652 text-overflow: ellipsis;
1653 1653 }
1654 1654
1655 1655 table.group_members {
1656 1656 width: 100%
1657 1657 }
1658 1658
1659 1659 .group_members {
1660 1660 margin-top: 0;
1661 1661 padding: 0;
1662 1662
1663 1663 img {
1664 1664 height: @gravatar-size;
1665 1665 width: @gravatar-size;
1666 1666 margin-right: .5em;
1667 1667 margin-left: 3px;
1668 1668 }
1669 1669
1670 1670 .to-delete {
1671 1671 .user {
1672 1672 text-decoration: line-through;
1673 1673 }
1674 1674 }
1675 1675 }
1676 1676
1677 1677 .compare_view_commits_title {
1678 1678 .disabled {
1679 1679 cursor: inherit;
1680 1680 &:hover{
1681 1681 background-color: inherit;
1682 1682 color: inherit;
1683 1683 }
1684 1684 }
1685 1685 }
1686 1686
1687 1687 .subtitle-compare {
1688 1688 margin: -15px 0px 0px 0px;
1689 1689 }
1690 1690
1691 1691 // new entry in group_members
1692 1692 .td-author-new-entry {
1693 1693 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1694 1694 }
1695 1695
1696 1696 .usergroup_member_remove {
1697 1697 width: 16px;
1698 1698 margin-bottom: 10px;
1699 1699 padding: 0;
1700 1700 color: black !important;
1701 1701 cursor: pointer;
1702 1702 }
1703 1703
1704 1704 .reviewer_ac .ac-input {
1705 1705 width: 98%;
1706 1706 margin-bottom: 1em;
1707 1707 }
1708 1708
1709 1709 .observer_ac .ac-input {
1710 1710 width: 98%;
1711 1711 margin-bottom: 1em;
1712 1712 }
1713 1713
1714 1714 .rule-table {
1715 1715 width: 100%;
1716 1716 }
1717 1717
1718 1718 .rule-table td {
1719 1719
1720 1720 }
1721 1721
1722 1722 .rule-table .td-role {
1723 1723 width: 100px
1724 1724 }
1725 1725
1726 1726 .rule-table .td-mandatory {
1727 1727 width: 100px
1728 1728 }
1729 1729
1730 1730 .rule-table .td-group-votes {
1731 1731 width: 150px
1732 1732 }
1733 1733
1734 1734 .compare_view_commits tr{
1735 1735 height: 20px;
1736 1736 }
1737 1737 .compare_view_commits td {
1738 1738 vertical-align: top;
1739 1739 padding-top: 10px;
1740 1740 }
1741 1741 .compare_view_commits .author {
1742 1742 margin-left: 5px;
1743 1743 }
1744 1744
1745 1745 .compare_view_commits {
1746 1746 .color-a {
1747 1747 color: @alert1;
1748 1748 }
1749 1749
1750 1750 .color-c {
1751 1751 color: @color3;
1752 1752 }
1753 1753
1754 1754 .color-r {
1755 1755 color: @color5;
1756 1756 }
1757 1757
1758 1758 .color-a-bg {
1759 1759 background-color: @alert1;
1760 1760 }
1761 1761
1762 1762 .color-c-bg {
1763 1763 background-color: @alert3;
1764 1764 }
1765 1765
1766 1766 .color-r-bg {
1767 1767 background-color: @alert2;
1768 1768 }
1769 1769
1770 1770 .color-a-border {
1771 1771 border: 1px solid @alert1;
1772 1772 }
1773 1773
1774 1774 .color-c-border {
1775 1775 border: 1px solid @alert3;
1776 1776 }
1777 1777
1778 1778 .color-r-border {
1779 1779 border: 1px solid @alert2;
1780 1780 }
1781 1781
1782 1782 .commit-change-indicator {
1783 1783 width: 15px;
1784 1784 height: 15px;
1785 1785 position: relative;
1786 1786 left: 15px;
1787 1787 }
1788 1788
1789 1789 .commit-change-content {
1790 1790 text-align: center;
1791 1791 vertical-align: middle;
1792 1792 line-height: 15px;
1793 1793 }
1794 1794 }
1795 1795
1796 1796 .compare_view_filepath {
1797 1797 color: @grey1;
1798 1798 }
1799 1799
1800 1800 .show_more {
1801 1801 display: inline-block;
1802 1802 width: 0;
1803 1803 height: 0;
1804 1804 vertical-align: middle;
1805 1805 content: "";
1806 1806 border: 4px solid;
1807 1807 border-right-color: transparent;
1808 1808 border-bottom-color: transparent;
1809 1809 border-left-color: transparent;
1810 1810 font-size: 0;
1811 1811 }
1812 1812
1813 1813 .journal_more .show_more {
1814 1814 display: inline;
1815 1815
1816 1816 &:after {
1817 1817 content: none;
1818 1818 }
1819 1819 }
1820 1820
1821 1821 .compare_view_commits .collapse_commit:after {
1822 1822 cursor: pointer;
1823 1823 content: "\00A0\25B4";
1824 1824 margin-left: -3px;
1825 1825 font-size: 17px;
1826 1826 color: @grey4;
1827 1827 }
1828 1828
1829 1829 .diff_links {
1830 1830 margin-left: 8px;
1831 1831 }
1832 1832
1833 1833 #pull_request_overview {
1834 1834 div.ancestor {
1835 1835 margin: -33px 0;
1836 1836 }
1837 1837 }
1838 1838
1839 1839 div.ancestor {
1840 1840
1841 1841 }
1842 1842
1843 1843 .cs_icon_td input[type="checkbox"] {
1844 1844 display: none;
1845 1845 }
1846 1846
1847 1847 .cs_icon_td .expand_file_icon:after {
1848 1848 cursor: pointer;
1849 1849 content: "\00A0\25B6";
1850 1850 font-size: 12px;
1851 1851 color: @grey4;
1852 1852 }
1853 1853
1854 1854 .cs_icon_td .collapse_file_icon:after {
1855 1855 cursor: pointer;
1856 1856 content: "\00A0\25BC";
1857 1857 font-size: 12px;
1858 1858 color: @grey4;
1859 1859 }
1860 1860
1861 1861 /*new binary
1862 1862 NEW_FILENODE = 1
1863 1863 DEL_FILENODE = 2
1864 1864 MOD_FILENODE = 3
1865 1865 RENAMED_FILENODE = 4
1866 1866 COPIED_FILENODE = 5
1867 1867 CHMOD_FILENODE = 6
1868 1868 BIN_FILENODE = 7
1869 1869 */
1870 1870 .cs_files_expand {
1871 1871 font-size: @basefontsize + 5px;
1872 1872 line-height: 1.8em;
1873 1873 float: right;
1874 1874 }
1875 1875
1876 1876 .cs_files_expand span{
1877 1877 color: @rcblue;
1878 1878 cursor: pointer;
1879 1879 }
1880 1880 .cs_files {
1881 1881 clear: both;
1882 1882 padding-bottom: @padding;
1883 1883
1884 1884 .cur_cs {
1885 1885 margin: 10px 2px;
1886 1886 font-weight: bold;
1887 1887 }
1888 1888
1889 1889 .node {
1890 1890 float: left;
1891 1891 }
1892 1892
1893 1893 .changes {
1894 1894 float: right;
1895 1895 color: white;
1896 1896 font-size: @basefontsize - 4px;
1897 1897 margin-top: 4px;
1898 1898 opacity: 0.6;
1899 1899 filter: Alpha(opacity=60); /* IE8 and earlier */
1900 1900
1901 1901 .added {
1902 1902 background-color: @alert1;
1903 1903 float: left;
1904 1904 text-align: center;
1905 1905 }
1906 1906
1907 1907 .deleted {
1908 1908 background-color: @alert2;
1909 1909 float: left;
1910 1910 text-align: center;
1911 1911 }
1912 1912
1913 1913 .bin {
1914 1914 background-color: @alert1;
1915 1915 text-align: center;
1916 1916 }
1917 1917
1918 1918 /*new binary*/
1919 1919 .bin.bin1 {
1920 1920 background-color: @alert1;
1921 1921 text-align: center;
1922 1922 }
1923 1923
1924 1924 /*deleted binary*/
1925 1925 .bin.bin2 {
1926 1926 background-color: @alert2;
1927 1927 text-align: center;
1928 1928 }
1929 1929
1930 1930 /*mod binary*/
1931 1931 .bin.bin3 {
1932 1932 background-color: @grey2;
1933 1933 text-align: center;
1934 1934 }
1935 1935
1936 1936 /*rename file*/
1937 1937 .bin.bin4 {
1938 1938 background-color: @alert4;
1939 1939 text-align: center;
1940 1940 }
1941 1941
1942 1942 /*copied file*/
1943 1943 .bin.bin5 {
1944 1944 background-color: @alert4;
1945 1945 text-align: center;
1946 1946 }
1947 1947
1948 1948 /*chmod file*/
1949 1949 .bin.bin6 {
1950 1950 background-color: @grey2;
1951 1951 text-align: center;
1952 1952 }
1953 1953 }
1954 1954 }
1955 1955
1956 1956 .cs_files .cs_added, .cs_files .cs_A,
1957 1957 .cs_files .cs_added, .cs_files .cs_M,
1958 1958 .cs_files .cs_added, .cs_files .cs_D {
1959 1959 height: 16px;
1960 1960 padding-right: 10px;
1961 1961 margin-top: 7px;
1962 1962 text-align: left;
1963 1963 }
1964 1964
1965 1965 .cs_icon_td {
1966 1966 min-width: 16px;
1967 1967 width: 16px;
1968 1968 }
1969 1969
1970 1970 .pull-request-merge {
1971 1971 border: 1px solid @grey5;
1972 1972 padding: 10px 0px 20px;
1973 1973 margin-top: 10px;
1974 1974 margin-bottom: 20px;
1975 1975 }
1976 1976
1977 1977 .pull-request-merge-refresh {
1978 1978 margin: 2px 7px;
1979 1979 a {
1980 1980 color: @grey3;
1981 1981 }
1982 1982 }
1983 1983
1984 1984 .pull-request-merge ul {
1985 1985 padding: 0px 0px;
1986 1986 }
1987 1987
1988 1988 .pull-request-merge li {
1989 1989 list-style-type: none;
1990 1990 }
1991 1991
1992 1992 .pull-request-merge .pull-request-wrap {
1993 1993 height: auto;
1994 1994 padding: 0px 0px;
1995 1995 text-align: right;
1996 1996 }
1997 1997
1998 1998 .pull-request-merge span {
1999 1999 margin-right: 5px;
2000 2000 }
2001 2001
2002 2002 .pull-request-merge-actions {
2003 2003 min-height: 30px;
2004 2004 padding: 0px 0px;
2005 2005 }
2006 2006
2007 2007 .pull-request-merge-info {
2008 2008 padding: 0px 5px 5px 0px;
2009 2009 }
2010 2010
2011 2011 .merge-status {
2012 2012 margin-right: 5px;
2013 2013 }
2014 2014
2015 2015 .merge-message {
2016 2016 font-size: 1.2em
2017 2017 }
2018 2018
2019 2019 .merge-message.success i,
2020 2020 .merge-icon.success i {
2021 2021 color:@alert1;
2022 2022 }
2023 2023
2024 2024 .merge-message.warning i,
2025 2025 .merge-icon.warning i {
2026 2026 color: @alert3;
2027 2027 }
2028 2028
2029 2029 .merge-message.error i,
2030 2030 .merge-icon.error i {
2031 2031 color:@alert2;
2032 2032 }
2033 2033
2034 2034 .pr-versions {
2035 2035 font-size: 1.1em;
2036 2036 padding: 7.5px;
2037 2037
2038 2038 table {
2039 2039
2040 2040 }
2041 2041
2042 2042 td {
2043 2043 line-height: 15px;
2044 2044 }
2045 2045
2046 2046 .compare-radio-button {
2047 2047 position: relative;
2048 2048 top: -3px;
2049 2049 }
2050 2050 }
2051 2051
2052 2052
2053 2053 #close_pull_request {
2054 2054 margin-right: 0px;
2055 2055 }
2056 2056
2057 2057 .empty_data {
2058 2058 color: @grey4;
2059 2059 }
2060 2060
2061 2061 #changeset_compare_view_content {
2062 2062 clear: both;
2063 2063 width: 100%;
2064 2064 box-sizing: border-box;
2065 2065 .border-radius(@border-radius);
2066 2066
2067 2067 .help-block {
2068 2068 margin: @padding 0;
2069 2069 color: @text-color;
2070 2070 &.pre-formatting {
2071 2071 white-space: pre;
2072 2072 }
2073 2073 }
2074 2074
2075 2075 .empty_data {
2076 2076 margin: @padding 0;
2077 2077 }
2078 2078
2079 2079 .alert {
2080 2080 margin-bottom: @space;
2081 2081 }
2082 2082 }
2083 2083
2084 2084 .table_disp {
2085 2085 .status {
2086 2086 width: auto;
2087 2087 }
2088 2088 }
2089 2089
2090 2090
2091 2091 .creation_in_progress {
2092 2092 color: @grey4
2093 2093 }
2094 2094
2095 2095 .status_box_menu {
2096 2096 margin: 0;
2097 2097 }
2098 2098
2099 2099 .notification-table{
2100 2100 margin-bottom: @space;
2101 2101 display: table;
2102 2102 width: 100%;
2103 2103
2104 2104 .container{
2105 2105 display: table-row;
2106 2106
2107 2107 .notification-header{
2108 2108 border-bottom: @border-thickness solid @border-default-color;
2109 2109 }
2110 2110
2111 2111 .notification-subject{
2112 2112 display: table-cell;
2113 2113 }
2114 2114 }
2115 2115 }
2116 2116
2117 2117 // Notifications
2118 2118 .notification-header{
2119 2119 display: table;
2120 2120 width: 100%;
2121 2121 padding: floor(@basefontsize/2) 0;
2122 2122 line-height: 1em;
2123 2123
2124 2124 .desc, .delete-notifications, .read-notifications{
2125 2125 display: table-cell;
2126 2126 text-align: left;
2127 2127 }
2128 2128
2129 2129 .delete-notifications, .read-notifications{
2130 2130 width: 35px;
2131 2131 min-width: 35px; //fixes when only one button is displayed
2132 2132 }
2133 2133 }
2134 2134
2135 2135 .notification-body {
2136 2136 .markdown-block,
2137 2137 .rst-block {
2138 2138 padding: @padding 0;
2139 2139 }
2140 2140
2141 2141 .notification-subject {
2142 2142 padding: @textmargin 0;
2143 2143 border-bottom: @border-thickness solid @border-default-color;
2144 2144 }
2145 2145 }
2146 2146
2147 2147 .notice-messages {
2148 2148 .markdown-block,
2149 2149 .rst-block {
2150 2150 padding: 0;
2151 2151 }
2152 2152 }
2153 2153
2154 2154 .notifications_buttons{
2155 2155 float: right;
2156 2156 }
2157 2157
2158 2158 #notification-status{
2159 2159 display: inline;
2160 2160 }
2161 2161
2162 2162 // Repositories
2163 2163
2164 2164 #summary.fields{
2165 2165 display: table;
2166 2166
2167 2167 .field{
2168 2168 display: table-row;
2169 2169
2170 2170 .label-summary{
2171 2171 display: table-cell;
2172 2172 min-width: @label-summary-minwidth;
2173 2173 padding-top: @padding/2;
2174 2174 padding-bottom: @padding/2;
2175 2175 padding-right: @padding/2;
2176 2176 }
2177 2177
2178 2178 .input{
2179 2179 display: table-cell;
2180 2180 padding: @padding/2;
2181 2181
2182 2182 input{
2183 2183 min-width: 29em;
2184 2184 padding: @padding/4;
2185 2185 }
2186 2186 }
2187 2187 .statistics, .downloads{
2188 2188 .disabled{
2189 2189 color: @grey4;
2190 2190 }
2191 2191 }
2192 2192 }
2193 2193 }
2194 2194
2195 2195 #summary{
2196 2196 width: 70%;
2197 2197 }
2198 2198
2199 2199
2200 2200 // Journal
2201 2201 .journal.title {
2202 2202 h5 {
2203 2203 float: left;
2204 2204 margin: 0;
2205 2205 width: 70%;
2206 2206 }
2207 2207
2208 2208 ul {
2209 2209 float: right;
2210 2210 display: inline-block;
2211 2211 margin: 0;
2212 2212 width: 30%;
2213 2213 text-align: right;
2214 2214
2215 2215 li {
2216 2216 display: inline;
2217 2217 font-size: @journal-fontsize;
2218 2218 line-height: 1em;
2219 2219
2220 2220 list-style-type: none;
2221 2221 }
2222 2222 }
2223 2223 }
2224 2224
2225 2225 .filterexample {
2226 2226 position: absolute;
2227 2227 top: 95px;
2228 2228 left: @contentpadding;
2229 2229 color: @rcblue;
2230 2230 font-size: 11px;
2231 2231 font-family: @text-regular;
2232 2232 cursor: help;
2233 2233
2234 2234 &:hover {
2235 2235 color: @rcdarkblue;
2236 2236 }
2237 2237
2238 2238 @media (max-width:768px) {
2239 2239 position: relative;
2240 2240 top: auto;
2241 2241 left: auto;
2242 2242 display: block;
2243 2243 }
2244 2244 }
2245 2245
2246 2246
2247 2247 #journal{
2248 2248 margin-bottom: @space;
2249 2249
2250 2250 .journal_day{
2251 2251 margin-bottom: @textmargin/2;
2252 2252 padding-bottom: @textmargin/2;
2253 2253 font-size: @journal-fontsize;
2254 2254 border-bottom: @border-thickness solid @border-default-color;
2255 2255 }
2256 2256
2257 2257 .journal_container{
2258 2258 margin-bottom: @space;
2259 2259
2260 2260 .journal_user{
2261 2261 display: inline-block;
2262 2262 }
2263 2263 .journal_action_container{
2264 2264 display: block;
2265 2265 margin-top: @textmargin;
2266 2266
2267 2267 div{
2268 2268 display: inline;
2269 2269 }
2270 2270
2271 2271 div.journal_action_params{
2272 2272 display: block;
2273 2273 }
2274 2274
2275 2275 div.journal_repo:after{
2276 2276 content: "\A";
2277 2277 white-space: pre;
2278 2278 }
2279 2279
2280 2280 div.date{
2281 2281 display: block;
2282 2282 margin-bottom: @textmargin;
2283 2283 }
2284 2284 }
2285 2285 }
2286 2286 }
2287 2287
2288 2288 // Files
2289 2289 .edit-file-title {
2290 2290 font-size: 16px;
2291 2291
2292 2292 .title-heading {
2293 2293 padding: 2px;
2294 2294 }
2295 2295 }
2296 2296
2297 2297 .edit-file-fieldset {
2298 2298 margin: @sidebarpadding 0;
2299 2299
2300 2300 .fieldset {
2301 2301 .left-label {
2302 2302 width: 13%;
2303 2303 }
2304 2304 .right-content {
2305 2305 width: 87%;
2306 2306 max-width: 100%;
2307 2307 }
2308 2308 .filename-label {
2309 2309 margin-top: 13px;
2310 2310 }
2311 2311 .commit-message-label {
2312 2312 margin-top: 4px;
2313 2313 }
2314 2314 .file-upload-input {
2315 2315 input {
2316 2316 display: none;
2317 2317 }
2318 2318 margin-top: 10px;
2319 2319 }
2320 2320 .file-upload-label {
2321 2321 margin-top: 10px;
2322 2322 }
2323 2323 p {
2324 2324 margin-top: 5px;
2325 2325 }
2326 2326
2327 2327 }
2328 2328 .custom-path-link {
2329 2329 margin-left: 5px;
2330 2330 }
2331 2331 #commit {
2332 2332 resize: vertical;
2333 2333 }
2334 2334 }
2335 2335
2336 2336 .delete-file-preview {
2337 2337 max-height: 250px;
2338 2338 }
2339 2339
2340 2340 .new-file,
2341 2341 #filter_activate,
2342 2342 #filter_deactivate {
2343 2343 float: right;
2344 2344 margin: 0 0 0 10px;
2345 2345 }
2346 2346
2347 2347 .file-upload-transaction-wrapper {
2348 2348 margin-top: 57px;
2349 2349 clear: both;
2350 2350 }
2351 2351
2352 2352 .file-upload-transaction-wrapper .error {
2353 2353 color: @color5;
2354 2354 }
2355 2355
2356 2356 .file-upload-transaction {
2357 2357 min-height: 200px;
2358 2358 padding: 54px;
2359 2359 border: 1px solid @grey5;
2360 2360 text-align: center;
2361 2361 clear: both;
2362 2362 }
2363 2363
2364 2364 .file-upload-transaction i {
2365 2365 font-size: 48px
2366 2366 }
2367 2367
2368 2368 h3.files_location{
2369 2369 line-height: 2.4em;
2370 2370 }
2371 2371
2372 2372 .browser-nav {
2373 2373 width: 100%;
2374 2374 display: table;
2375 2375 margin-bottom: 20px;
2376 2376
2377 2377 .info_box {
2378 2378 float: left;
2379 2379 display: inline-table;
2380 2380 height: 2.5em;
2381 2381
2382 2382 .browser-cur-rev, .info_box_elem {
2383 2383 display: table-cell;
2384 2384 vertical-align: middle;
2385 2385 }
2386 2386
2387 2387 .drop-menu {
2388 2388 margin: 0 10px;
2389 2389 }
2390 2390
2391 2391 .info_box_elem {
2392 2392 border-top: @border-thickness solid @grey5;
2393 2393 border-bottom: @border-thickness solid @grey5;
2394 2394 box-shadow: @button-shadow;
2395 2395
2396 2396 #at_rev, a {
2397 2397 padding: 0.6em 0.4em;
2398 2398 margin: 0;
2399 2399 .box-shadow(none);
2400 2400 border: 0;
2401 2401 height: 12px;
2402 2402 color: @grey2;
2403 2403 }
2404 2404
2405 2405 input#at_rev {
2406 2406 max-width: 50px;
2407 2407 text-align: center;
2408 2408 }
2409 2409
2410 2410 &.previous {
2411 2411 border: @border-thickness solid @grey5;
2412 2412 border-top-left-radius: @border-radius;
2413 2413 border-bottom-left-radius: @border-radius;
2414 2414
2415 2415 &:hover {
2416 2416 border-color: @grey4;
2417 2417 }
2418 2418
2419 2419 .disabled {
2420 2420 color: @grey5;
2421 2421 cursor: not-allowed;
2422 2422 opacity: 0.5;
2423 2423 }
2424 2424 }
2425 2425
2426 2426 &.next {
2427 2427 border: @border-thickness solid @grey5;
2428 2428 border-top-right-radius: @border-radius;
2429 2429 border-bottom-right-radius: @border-radius;
2430 2430
2431 2431 &:hover {
2432 2432 border-color: @grey4;
2433 2433 }
2434 2434
2435 2435 .disabled {
2436 2436 color: @grey5;
2437 2437 cursor: not-allowed;
2438 2438 opacity: 0.5;
2439 2439 }
2440 2440 }
2441 2441 }
2442 2442
2443 2443 .browser-cur-rev {
2444 2444
2445 2445 span{
2446 2446 margin: 0;
2447 2447 color: @rcblue;
2448 2448 height: 12px;
2449 2449 display: inline-block;
2450 2450 padding: 0.7em 1em ;
2451 2451 border: @border-thickness solid @rcblue;
2452 2452 margin-right: @padding;
2453 2453 }
2454 2454 }
2455 2455
2456 2456 }
2457 2457
2458 2458 .select-index-number {
2459 2459 margin: 0 0 0 20px;
2460 2460 color: @grey3;
2461 2461 }
2462 2462
2463 2463 .search_activate {
2464 2464 display: table-cell;
2465 2465 vertical-align: middle;
2466 2466
2467 2467 input, label{
2468 2468 margin: 0;
2469 2469 padding: 0;
2470 2470 }
2471 2471
2472 2472 input{
2473 2473 margin-left: @textmargin;
2474 2474 }
2475 2475
2476 2476 }
2477 2477 }
2478 2478
2479 2479 .browser-cur-rev{
2480 2480 margin-bottom: @textmargin;
2481 2481 }
2482 2482
2483 2483 #node_filter_box_loading{
2484 2484 .info_text;
2485 2485 }
2486 2486
2487 2487 .browser-search {
2488 2488 margin: -25px 0px 5px 0px;
2489 2489 }
2490 2490
2491 2491 .files-quick-filter {
2492 2492 float: right;
2493 2493 width: 180px;
2494 2494 position: relative;
2495 2495 }
2496 2496
2497 2497 .files-filter-box {
2498 2498 display: flex;
2499 2499 padding: 0px;
2500 2500 border-radius: 3px;
2501 2501 margin-bottom: 0;
2502 2502
2503 2503 a {
2504 2504 border: none !important;
2505 2505 }
2506 2506
2507 2507 li {
2508 2508 list-style-type: none
2509 2509 }
2510 2510 }
2511 2511
2512 2512 .files-filter-box-path {
2513 2513 line-height: 33px;
2514 2514 padding: 0;
2515 2515 width: 20px;
2516 2516 position: absolute;
2517 2517 z-index: 11;
2518 2518 left: 5px;
2519 2519 }
2520 2520
2521 2521 .files-filter-box-input {
2522 2522 margin-right: 0;
2523 2523
2524 2524 input {
2525 2525 border: 1px solid @white;
2526 2526 padding-left: 25px;
2527 2527 width: 145px;
2528 2528
2529 2529 &:hover {
2530 2530 border-color: @grey6;
2531 2531 }
2532 2532
2533 2533 &:focus {
2534 2534 border-color: @grey5;
2535 2535 }
2536 2536 }
2537 2537 }
2538 2538
2539 2539 .browser-result{
2540 2540 td a{
2541 2541 margin-left: 0.5em;
2542 2542 display: inline-block;
2543 2543
2544 2544 em {
2545 2545 font-weight: @text-bold-weight;
2546 2546 font-family: @text-bold;
2547 2547 }
2548 2548 }
2549 2549 }
2550 2550
2551 2551 .browser-highlight{
2552 2552 background-color: @grey5-alpha;
2553 2553 }
2554 2554
2555 2555
2556 2556 .edit-file-fieldset #location,
2557 2557 .edit-file-fieldset #filename {
2558 2558 display: flex;
2559 2559 width: -moz-available; /* WebKit-based browsers will ignore this. */
2560 2560 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2561 2561 width: fill-available;
2562 2562 border: 0;
2563 2563 }
2564 2564
2565 2565 .path-items {
2566 2566 display: flex;
2567 2567 padding: 0;
2568 2568 border: 1px solid #eeeeee;
2569 2569 width: 100%;
2570 2570 float: left;
2571 2571
2572 2572 .breadcrumb-path {
2573 2573 line-height: 30px;
2574 2574 padding: 0 4px;
2575 2575 white-space: nowrap;
2576 2576 }
2577 2577
2578 2578 .upload-form {
2579 2579 margin-top: 46px;
2580 2580 }
2581 2581
2582 2582 .location-path {
2583 2583 width: -moz-available; /* WebKit-based browsers will ignore this. */
2584 2584 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2585 2585 width: fill-available;
2586 2586
2587 2587 .file-name-input {
2588 2588 padding: 0.5em 0;
2589 2589 }
2590 2590
2591 2591 }
2592 2592
2593 2593 ul {
2594 2594 display: flex;
2595 2595 margin: 0;
2596 2596 padding: 0;
2597 2597 width: 100%;
2598 2598 }
2599 2599
2600 2600 li {
2601 2601 list-style-type: none;
2602 2602 }
2603 2603
2604 2604 }
2605 2605
2606 2606 .editor-items {
2607 2607 height: 40px;
2608 2608 margin: 10px 0 -17px 10px;
2609 2609
2610 2610 .editor-action {
2611 2611 cursor: pointer;
2612 2612 }
2613 2613
2614 2614 .editor-action.active {
2615 2615 border-bottom: 2px solid #5C5C5C;
2616 2616 }
2617 2617
2618 2618 li {
2619 2619 list-style-type: none;
2620 2620 }
2621 2621 }
2622 2622
2623 2623 .edit-file-fieldset .message textarea {
2624 2624 border: 1px solid #eeeeee;
2625 2625 }
2626 2626
2627 2627 #files_data .codeblock {
2628 2628 background-color: #F5F5F5;
2629 2629 }
2630 2630
2631 2631 #editor_preview {
2632 2632 background: white;
2633 2633 }
2634 2634
2635 2635 .show-editor {
2636 2636 padding: 10px;
2637 2637 background-color: white;
2638 2638
2639 2639 }
2640 2640
2641 2641 .show-preview {
2642 2642 padding: 10px;
2643 2643 background-color: white;
2644 2644 border-left: 1px solid #eeeeee;
2645 2645 }
2646 2646 // quick filter
2647 2647 .grid-quick-filter {
2648 2648 float: right;
2649 2649 position: relative;
2650 2650 }
2651 2651
2652 2652 .grid-filter-box {
2653 2653 display: flex;
2654 2654 padding: 0px;
2655 2655 border-radius: 3px;
2656 2656 margin-bottom: 0;
2657 2657
2658 2658 a {
2659 2659 border: none !important;
2660 2660 }
2661 2661
2662 2662 li {
2663 2663 list-style-type: none
2664 2664 }
2665 2665
2666 2666 }
2667 2667
2668 2668 .grid-filter-box-icon {
2669 2669 line-height: 33px;
2670 2670 padding: 0;
2671 2671 width: 20px;
2672 2672 position: absolute;
2673 2673 z-index: 11;
2674 2674 left: 5px;
2675 2675 }
2676 2676
2677 2677 .grid-filter-box-input {
2678 2678 margin-right: 0;
2679 2679
2680 2680 input {
2681 2681 border: 1px solid @white;
2682 2682 padding-left: 25px;
2683 2683 width: 145px;
2684 2684
2685 2685 &:hover {
2686 2686 border-color: @grey6;
2687 2687 }
2688 2688
2689 2689 &:focus {
2690 2690 border-color: @grey5;
2691 2691 }
2692 2692 }
2693 2693 }
2694 2694
2695 2695
2696 2696
2697 2697 // Search
2698 2698
2699 2699 .search-form{
2700 2700 #q {
2701 2701 width: @search-form-width;
2702 2702 }
2703 2703 .fields{
2704 2704 margin: 0 0 @space;
2705 2705 }
2706 2706
2707 2707 label{
2708 2708 display: inline-block;
2709 2709 margin-right: @textmargin;
2710 2710 padding-top: 0.25em;
2711 2711 }
2712 2712
2713 2713
2714 2714 .results{
2715 2715 clear: both;
2716 2716 margin: 0 0 @padding;
2717 2717 }
2718 2718
2719 2719 .search-tags {
2720 2720 padding: 5px 0;
2721 2721 }
2722 2722 }
2723 2723
2724 2724 div.search-feedback-items {
2725 2725 display: inline-block;
2726 2726 }
2727 2727
2728 2728 div.search-code-body {
2729 2729 background-color: #ffffff; padding: 5px 0 5px 10px;
2730 2730 pre {
2731 2731 .match { background-color: #faffa6;}
2732 2732 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2733 2733 }
2734 2734 }
2735 2735
2736 2736 .expand_commit.search {
2737 2737 .show_more.open {
2738 2738 height: auto;
2739 2739 max-height: none;
2740 2740 }
2741 2741 }
2742 2742
2743 2743 .search-results {
2744 2744
2745 2745 h2 {
2746 2746 margin-bottom: 0;
2747 2747 }
2748 2748 .codeblock {
2749 2749 border: none;
2750 2750 background: transparent;
2751 2751 }
2752 2752
2753 2753 .codeblock-header {
2754 2754 border: none;
2755 2755 background: transparent;
2756 2756 }
2757 2757
2758 2758 .code-body {
2759 2759 border: @border-thickness solid @grey6;
2760 2760 .border-radius(@border-radius);
2761 2761 }
2762 2762
2763 2763 .td-commit {
2764 2764 &:extend(pre);
2765 2765 border-bottom: @border-thickness solid @border-default-color;
2766 2766 }
2767 2767
2768 2768 .message {
2769 2769 height: auto;
2770 2770 max-width: 350px;
2771 2771 white-space: normal;
2772 2772 text-overflow: initial;
2773 2773 overflow: visible;
2774 2774
2775 2775 .match { background-color: #faffa6;}
2776 2776 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2777 2777 }
2778 2778
2779 2779 .path {
2780 2780 border-bottom: none !important;
2781 2781 border-left: 1px solid @grey6 !important;
2782 2782 border-right: 1px solid @grey6 !important;
2783 2783 }
2784 2784 }
2785 2785
2786 2786 table.rctable td.td-search-results div {
2787 2787 max-width: 100%;
2788 2788 }
2789 2789
2790 2790 #tip-box, .tip-box{
2791 2791 padding: @menupadding/2;
2792 2792 display: block;
2793 2793 border: @border-thickness solid @border-highlight-color;
2794 2794 .border-radius(@border-radius);
2795 2795 background-color: white;
2796 2796 z-index: 99;
2797 2797 white-space: pre-wrap;
2798 2798 }
2799 2799
2800 2800 #linktt {
2801 2801 width: 79px;
2802 2802 }
2803 2803
2804 2804 #help_kb .modal-content{
2805 2805 max-width: 800px;
2806 2806 margin: 10% auto;
2807 2807
2808 2808 table{
2809 2809 td,th{
2810 2810 border-bottom: none;
2811 2811 line-height: 2.5em;
2812 2812 }
2813 2813 th{
2814 2814 padding-bottom: @textmargin/2;
2815 2815 }
2816 2816 td.keys{
2817 2817 text-align: center;
2818 2818 }
2819 2819 }
2820 2820
2821 2821 .block-left{
2822 2822 width: 45%;
2823 2823 margin-right: 5%;
2824 2824 }
2825 2825 .modal-footer{
2826 2826 clear: both;
2827 2827 }
2828 2828 .key.tag{
2829 2829 padding: 0.5em;
2830 2830 background-color: @rcblue;
2831 2831 color: white;
2832 2832 border-color: @rcblue;
2833 2833 .box-shadow(none);
2834 2834 }
2835 2835 }
2836 2836
2837 2837
2838 2838
2839 2839 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2840 2840
2841 2841 @import 'statistics-graph';
2842 2842 @import 'tables';
2843 2843 @import 'forms';
2844 2844 @import 'diff';
2845 2845 @import 'summary';
2846 2846 @import 'navigation';
2847 2847
2848 2848 //--- SHOW/HIDE SECTIONS --//
2849 2849
2850 2850 .btn-collapse {
2851 2851 float: right;
2852 2852 text-align: right;
2853 2853 font-family: @text-light;
2854 2854 font-size: @basefontsize;
2855 2855 cursor: pointer;
2856 2856 border: none;
2857 2857 color: @rcblue;
2858 2858 }
2859 2859
2860 2860 table.rctable,
2861 2861 table.dataTable {
2862 2862 .btn-collapse {
2863 2863 float: right;
2864 2864 text-align: right;
2865 2865 }
2866 2866 }
2867 2867
2868 2868 table.rctable {
2869 2869 &.permissions {
2870 2870
2871 2871 th.td-owner {
2872 2872 padding: 0;
2873 2873 }
2874 2874
2875 2875 th {
2876 2876 font-weight: normal;
2877 2877 padding: 0 5px;
2878 2878 }
2879 2879
2880 2880 }
2881 2881 }
2882 2882
2883 2883
2884 2884 // TODO: johbo: Fix for IE10, this avoids that we see a border
2885 2885 // and padding around checkboxes and radio boxes. Move to the right place,
2886 2886 // or better: Remove this once we did the form refactoring.
2887 2887 input[type=checkbox],
2888 2888 input[type=radio] {
2889 2889 padding: 0;
2890 2890 border: none;
2891 2891 }
2892 2892
2893 2893 .toggle-ajax-spinner{
2894 2894 height: 16px;
2895 2895 width: 16px;
2896 2896 }
2897 2897
2898 2898
2899 2899 .markup-form .clearfix {
2900 2900 .border-radius(@border-radius);
2901 2901 margin: 0px;
2902 2902 }
2903 2903
2904 2904 .markup-form-area {
2905 2905 padding: 8px 12px;
2906 2906 border: 1px solid @grey4;
2907 2907 .border-radius(@border-radius);
2908 2908 }
2909 2909
2910 2910 .markup-form-area-header .nav-links {
2911 2911 display: flex;
2912 2912 flex-flow: row wrap;
2913 2913 -webkit-flex-flow: row wrap;
2914 2914 width: 100%;
2915 2915 }
2916 2916
2917 2917 .markup-form-area-footer {
2918 2918 display: flex;
2919 2919 }
2920 2920
2921 2921 .markup-form-area-footer .toolbar {
2922 2922
2923 2923 }
2924 2924
2925 2925 // markup Form
2926 2926 div.markup-form {
2927 2927 margin-top: 20px;
2928 2928 }
2929 2929
2930 2930 .markup-form strong {
2931 2931 display: block;
2932 2932 margin-bottom: 15px;
2933 2933 }
2934 2934
2935 2935 .markup-form textarea {
2936 2936 width: 100%;
2937 2937 height: 100px;
2938 2938 font-family: @text-monospace;
2939 2939 }
2940 2940
2941 2941 form.markup-form {
2942 2942 margin-top: 10px;
2943 2943 margin-left: 10px;
2944 2944 }
2945 2945
2946 2946 .markup-form .comment-block-ta,
2947 2947 .markup-form .preview-box {
2948 2948 .border-radius(@border-radius);
2949 2949 .box-sizing(border-box);
2950 2950 background-color: white;
2951 2951 }
2952 2952
2953 2953 .markup-form .preview-box.unloaded {
2954 2954 height: 50px;
2955 2955 text-align: center;
2956 2956 padding: 20px;
2957 2957 background-color: white;
2958 2958 }
2959 2959
2960 2960
2961 2961 .dropzone-wrapper {
2962 2962 border: 1px solid @grey5;
2963 2963 padding: 20px;
2964 2964 }
2965 2965
2966 2966 .dropzone,
2967 2967 .dropzone-pure {
2968 2968 border: 2px dashed @grey5;
2969 2969 border-radius: 5px;
2970 2970 background: white;
2971 2971 min-height: 200px;
2972 2972 padding: 54px;
2973 2973
2974 2974 .dz-message {
2975 2975 font-weight: 700;
2976 2976 text-align: center;
2977 2977 margin: 2em 0;
2978 2978 }
2979 2979
2980 2980 }
2981 2981
2982 2982 .dz-preview {
2983 2983 margin: 10px 0 !important;
2984 2984 position: relative;
2985 2985 vertical-align: top;
2986 2986 padding: 10px;
2987 2987 border-bottom: 1px solid @grey5;
2988 2988 }
2989 2989
2990 2990 .dz-filename {
2991 2991 font-weight: 700;
2992 2992 float: left;
2993 2993 }
2994 2994
2995 2995 .dz-sending {
2996 2996 float: right;
2997 2997 }
2998 2998
2999 2999 .dz-response {
3000 3000 clear: both
3001 3001 }
3002 3002
3003 3003 .dz-filename-size {
3004 3004 float: right
3005 3005 }
3006 3006
3007 3007 .dz-error-message {
3008 3008 color: @alert2;
3009 3009 padding-top: 10px;
3010 3010 clear: both;
3011 3011 }
3012 3012
3013 3013
3014 3014 .user-hovercard {
3015 3015 padding: 5px;
3016 3016 }
3017 3017
3018 3018 .user-hovercard-icon {
3019 3019 display: inline;
3020 3020 padding: 0;
3021 3021 box-sizing: content-box;
3022 3022 border-radius: 50%;
3023 3023 float: left;
3024 3024 }
3025 3025
3026 3026 .user-hovercard-name {
3027 3027 float: right;
3028 3028 vertical-align: top;
3029 3029 padding-left: 10px;
3030 3030 min-width: 150px;
3031 3031 }
3032 3032
3033 3033 .user-hovercard-bio {
3034 3034 clear: both;
3035 3035 padding-top: 10px;
3036 3036 }
3037 3037
3038 3038 .user-hovercard-header {
3039 3039 clear: both;
3040 3040 min-height: 10px;
3041 3041 }
3042 3042
3043 3043 .user-hovercard-footer {
3044 3044 clear: both;
3045 3045 min-height: 10px;
3046 3046 }
3047 3047
3048 3048 .user-group-hovercard {
3049 3049 padding: 5px;
3050 3050 }
3051 3051
3052 3052 .user-group-hovercard-icon {
3053 3053 display: inline;
3054 3054 padding: 0;
3055 3055 box-sizing: content-box;
3056 3056 border-radius: 50%;
3057 3057 float: left;
3058 3058 }
3059 3059
3060 3060 .user-group-hovercard-name {
3061 3061 float: left;
3062 3062 vertical-align: top;
3063 3063 padding-left: 10px;
3064 3064 min-width: 150px;
3065 3065 }
3066 3066
3067 3067 .user-group-hovercard-icon i {
3068 3068 border: 1px solid @grey4;
3069 3069 border-radius: 4px;
3070 3070 }
3071 3071
3072 3072 .user-group-hovercard-bio {
3073 3073 clear: both;
3074 3074 padding-top: 10px;
3075 3075 line-height: 1.0em;
3076 3076 }
3077 3077
3078 3078 .user-group-hovercard-header {
3079 3079 clear: both;
3080 3080 min-height: 10px;
3081 3081 }
3082 3082
3083 3083 .user-group-hovercard-footer {
3084 3084 clear: both;
3085 3085 min-height: 10px;
3086 3086 }
3087 3087
3088 3088 .pr-hovercard-header {
3089 3089 clear: both;
3090 3090 display: block;
3091 3091 line-height: 20px;
3092 3092 }
3093 3093
3094 3094 .pr-hovercard-user {
3095 3095 display: flex;
3096 3096 align-items: center;
3097 3097 padding-left: 5px;
3098 3098 }
3099 3099
3100 3100 .pr-hovercard-title {
3101 3101 padding-top: 5px;
3102 3102 }
3103 3103
3104 3104 .action-divider {
3105 3105 opacity: 0.5;
3106 3106 }
3107 3107
3108 3108 .details-inline-block {
3109 3109 display: inline-block;
3110 3110 position: relative;
3111 3111 }
3112 3112
3113 3113 .details-inline-block summary {
3114 3114 list-style: none;
3115 3115 }
3116 3116
3117 3117 details:not([open]) > :not(summary) {
3118 3118 display: none !important;
3119 3119 }
3120 3120
3121 3121 .details-reset > summary {
3122 3122 list-style: none;
3123 3123 }
3124 3124
3125 3125 .details-reset > summary::-webkit-details-marker {
3126 3126 display: none;
3127 3127 }
3128 3128
3129 3129 .details-dropdown {
3130 3130 position: absolute;
3131 3131 top: 100%;
3132 3132 width: 185px;
3133 3133 list-style: none;
3134 3134 background-color: #fff;
3135 3135 background-clip: padding-box;
3136 3136 border: 1px solid @grey5;
3137 3137 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3138 3138 left: -150px;
3139 3139 text-align: left;
3140 3140 z-index: 90;
3141 3141 }
3142 3142
3143 3143 .dropdown-divider {
3144 3144 display: block;
3145 3145 height: 0;
3146 3146 margin: 8px 0;
3147 3147 border-top: 1px solid @grey5;
3148 3148 }
3149 3149
3150 3150 .dropdown-item {
3151 3151 display: block;
3152 3152 padding: 4px 8px 4px 16px;
3153 3153 overflow: hidden;
3154 3154 text-overflow: ellipsis;
3155 3155 white-space: nowrap;
3156 3156 font-weight: normal;
3157 3157 }
3158 3158
3159 3159 .right-sidebar {
3160 3160 position: fixed;
3161 3161 top: 0px;
3162 3162 bottom: 0;
3163 3163 right: 0;
3164 3164
3165 3165 background: #fafafa;
3166 3166 z-index: 50;
3167 3167 }
3168 3168
3169 3169 .right-sidebar {
3170 3170 border-left: 1px solid @grey5;
3171 3171 }
3172 3172
3173 3173 .right-sidebar.right-sidebar-expanded {
3174 3174 width: 300px;
3175 3175 overflow: scroll;
3176 3176 }
3177 3177
3178 3178 .right-sidebar.right-sidebar-collapsed {
3179 3179 width: 40px;
3180 3180 padding: 0;
3181 3181 display: block;
3182 3182 overflow: hidden;
3183 3183 }
3184 3184
3185 3185 .sidenav {
3186 3186 float: right;
3187 3187 will-change: min-height;
3188 3188 background: #fafafa;
3189 3189 width: 100%;
3190 3190 }
3191 3191
3192 3192 .sidebar-toggle {
3193 3193 height: 30px;
3194 3194 text-align: center;
3195 3195 margin: 15px 0px 0 0;
3196 3196 }
3197 3197
3198 3198 .sidebar-toggle a {
3199 3199
3200 3200 }
3201 3201
3202 3202 .sidebar-content {
3203 3203 margin-left: 15px;
3204 3204 margin-right: 15px;
3205 3205 }
3206 3206
3207 3207 .sidebar-heading {
3208 3208 font-size: 1.2em;
3209 3209 font-weight: 700;
3210 3210 margin-top: 10px;
3211 3211 }
3212 3212
3213 3213 .sidebar-element {
3214 3214 margin-top: 20px;
3215 }
3215
3216 .icon-draft {
3217 color: @color-draft
3218 }
3219 }
3220
3216 3221
3217 3222 .right-sidebar-collapsed-state {
3218 3223 display: flex;
3219 3224 flex-direction: column;
3220 3225 justify-content: center;
3221 3226 align-items: center;
3222 3227 padding: 0 10px;
3223 3228 cursor: pointer;
3224 3229 font-size: 1.3em;
3225 3230 margin: 0 -15px;
3226 3231 }
3227 3232
3228 3233 .right-sidebar-collapsed-state:hover {
3229 3234 background-color: @grey5;
3230 3235 }
3231 3236
3232 3237 .old-comments-marker {
3233 3238 text-align: left;
3234 3239 }
3235 3240
3236 3241 .old-comments-marker td {
3237 3242 padding-top: 15px;
3238 border-bottom: 1px solid @grey5;
3239 }
3243 }
@@ -1,154 +1,156 b''
1 1 // variables for use in all RhodeCode products
2 2
3 3 // FONTS
4 4 //Primary Colors (brand)
5 5 @rcblue: #427cc9; //RhodeCode blue
6 6 @rcdarkblue: #305b91; //RhodeCode dark blue
7 7 @rclightblue: lighten(@rcblue, 30%);
8 8 @rchighlightblue: lighten(@rcblue, 35%);
9 9
10 10 // Secondary Colors (greyscale)
11 11 @black: #000;
12 12 @white: #fff;
13 13 @grey1: #2B2B2D; //midnight
14 14 @grey2: #5C5C5C; //charcoal
15 15 @grey3: #7E7F7F; //tungsten
16 16 @grey4: #949494; //light grey
17 17 @grey5: #dbd9da; //greyish
18 18 @grey6: #eeeeee; //silver
19 19 @grey7: #F5F5F5; //light silver
20 20
21 21 // special for navigation
22 22 @nav-grey: #CDCCCD;
23 23
24 24 @grey5-alpha: rgba(219, 217, 218, 0.3);
25 25
26 26 // Tertiary Colors
27 27 @color1: #879938; //olive green
28 28 @color2: #fcc93a; //bright yellow
29 29 @color3: #ff9e07; //orange-yellow
30 30 @color4: #fc663a; //bright orange
31 31 @color5: #d63d44; //signal red
32 32 @color6: #99287c; //violet
33 33 @color7: #682668; //dark purple
34 34 @color8: #194f8e; //dark blue
35 35
36 36 // Alert Colors (bright)
37 37 @alert1: #0ac878; //bright green
38 38 @alert2: #e85e4d; //soft red
39 39 @alert3: #ffc854; //corn yellow
40 40 @alert4: #84a5d2; //light blue
41 41
42 42 // Alert Inner Colors
43 43 @alert1-inner: #daf7eb; //bright green
44 44 @alert2-inner: #fbdfdb; //soft red
45 45 @alert3-inner: #fff4dd; //corn yellow
46 46 @alert4-inner: #e6edf6; //light blue
47 47
48 48 // Highlight color for lines and colors
49 49 @comment-highlight-color: #ffd887;
50 @color-draft: darken(@alert3, 30%);
51 @color-new: darken(@alert1, 5%);
50 52
51 53 // FONTS
52 54 @basefontsize: 13px;
53 55 @navigation-fontsize: 14px;
54 56 @journal-fontsize: @basefontsize+7px;
55 57 @text-color: @grey2;
56 58 @repo-title-fontsize: 18px;
57 59
58 60 @text-regular: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
59 61 @text-monospace: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
60 62
61 63 @text-italic: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
62 64
63 65 @text-bold: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
64 66 @text-bold-weight: 600;
65 67
66 68 @text-semibold: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
67 69 @text-semibold-weight: 500;
68 70
69 71 @text-bold-italic: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
70 72 @text-bold-italic-weight: 600;
71 73
72 74 @text-code: @text-monospace;
73 75 @text-light: @text-regular;
74 76
75 77
76 78 // Used for .close buttons
77 79 @text-bootstrap: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
78 80
79 81 @panel-title: @basefontsize;
80 82 @panel-footer: @basefontsize;
81 83
82 84 // BORDERS
83 85 @border-thickness: 1px;
84 86 @border-thickness-buttons: 1px;
85 87 @border-thickness-tags: 1px;
86 88 @border-radius: 2px;
87 89 @border-default-color: @grey5;
88 90 @border-highlight-color: @grey4;
89 91
90 92 // SPACING
91 93 @contentpadding: 15px; //padding on left and right of pages
92 94 @pagepadding: 40px; //padding on top and bottom of pages
93 95 @menupadding: 12px; //padding for sidebar and content
94 96 @sidebarpadding: 15px; //spacing between sections
95 97 @space: 40px; //spacing between sections
96 98 @padding: 15px; //padding inside modules
97 99 @textmargin: 20px; //spacing below headers
98 100 @header-padding: 20px;
99 101 @panel-padding: @padding;
100 102 @gravatar-size: 16px; // height/width of gravatar w/o border
101 103
102 104 // ADMIN
103 105 @form-max-width: 750px;
104 106
105 107 // FORMS (new)
106 108 @border-thickness-inputs: 1px;
107 109 @input-padding: .6em; //needs to match button padding
108 110 // TODO: johbo: Needed for working computation of paddings around labels etc.
109 111 // Expected to be replaced once we are done with the form refactoring.
110 112 @input-padding-px: 12px;
111 113 @legend-width: 220px;
112 114 @form-vertical-margin: 20px;
113 115 @form-check-width: 20px;
114 116 @form-radio-width: 10px;
115 117 @form-textcolor: @grey3;
116 118
117 119 // FORMS
118 120 @label-width: 220px;
119 121 //TODO: lisa: Eventually we don't need both of these; remove
120 122 // label-width when legend-width is no longer used
121 123
122 124 @input-border-thickness: @border-thickness;
123 125 @medium-inline-input-width: 115px;
124 126 @input-description-minwidth: 300px;
125 127 @label2-width: 200px;
126 128 @checkboxes-width: 420px;
127 129 @label-summary-minwidth: 80px;
128 130 @search-form-width: 400px;
129 131 @fields-input-m: 400px;
130 132 @fields-input-l: 720px;
131 133
132 134 // BUTTONS
133 135 @button-padding: .7em;
134 136 @button-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.07);
135 137
136 138 // DROPDOWNS
137 139 @dropdown-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.07);
138 140
139 141 // DEFAULT WIDTHS
140 142 @wrapper-maxwidth: 1600px;
141 143 @sidebar-width: 145px;
142 144 @sidebar-all-width: @sidebar-width + 2 * @sidebarpadding;
143 145 @sidebar-small-width: 100px;
144 146 @sidebar-small-all-width: @sidebar-small-width + 2 * @sidebarpadding;
145 147 @texteditor-width: 660px;
146 148 @maincontent-maxwidth: 940px;
147 149 @pullrequest-width: 1025px;
148 150 @summary-menu-stats-width: 200px;
149 151
150 152 // SCREEN WIDTHS
151 153 @screen-sm-min: 320px;
152 154
153 155 // For Bootstrap
154 156 @panel-border-radius: @border-radius;
@@ -1,205 +1,211 b''
1 1 import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
2 2 import '../channelstream-connection/channelstream-connection.js';
3 3 import '../rhodecode-toast/rhodecode-toast.js';
4 4 import '../rhodecode-favicon/rhodecode-favicon.js';
5 5
6 6 var ccLog = Logger.get('RhodeCodeApp');
7 7 ccLog.setLevel(Logger.OFF);
8 8
9 9 export class RhodecodeApp extends PolymerElement {
10 10
11 11 static get is() {
12 12 return 'rhodecode-app';
13 13 }
14 14
15 15 static get template(){
16 16 return html`
17 17 <channelstream-connection
18 18 id="channelstream-connection"
19 19 on-channelstream-listen-message="receivedMessage"
20 20 on-channelstream-connected="handleConnected"
21 21 on-channelstream-subscribed="handleSubscribed">
22 22 </channelstream-connection>
23 23 <rhodecode-favicon></rhodecode-favicon>
24 24 `
25 25 }
26 26
27 27 connectedCallback() {
28 28 super.connectedCallback();
29 29 ccLog.debug('rhodeCodeApp created');
30 30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
31 31 $.Topic('/comment').subscribe(this.handleComment.bind(this));
32 32 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
33 33 $.Topic('/connection_controller/subscribe').subscribe(
34 34 this.subscribeToChannelTopic.bind(this)
35 35 );
36 36
37 37 // this event can be used to coordinate plugins to do their
38 38 // initialization before channelstream is kicked off
39 39 $.Topic('/__MAIN_APP__').publish({});
40 40
41 41 for (var i = 0; i < alertMessagePayloads.length; i++) {
42 42 $.Topic('/notifications').publish(alertMessagePayloads[i]);
43 43 }
44 44 this.initPlugins();
45 45 // after rest of application loads and topics get fired, launch connection
46 46 $(document).ready(function () {
47 47 this.kickoffChannelstreamPlugin();
48 48 }.bind(this));
49 49 }
50 50
51 51 initPlugins() {
52 52 for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
53 53 var pluginDef = window.APPLICATION_PLUGINS[i];
54 54 if (pluginDef.component) {
55 55 var pluginElem = document.createElement(pluginDef.component);
56 56 this.shadowRoot.appendChild(pluginElem);
57 57 if (typeof pluginElem.init !== 'undefined') {
58 58 pluginElem.init();
59 59 }
60 60 }
61 61 }
62 62 }
63 63
64 64 /** proxy to channelstream connection */
65 65 getChannelStreamConnection() {
66 66 return this.$['channelstream-connection'];
67 67 }
68 68
69 69 handleNotifications(data) {
70 70 var elem = document.getElementById('notifications');
71 71 if (elem) {
72 72 elem.handleNotification(data);
73 73 }
74
75 74 }
76 75
77 76 handleComment(data) {
78 if (data.message.comment_id) {
77
78 if (data.message.comment_data.length !== 0) {
79 79 if (window.refreshAllComments !== undefined) {
80 80 refreshAllComments()
81 81 }
82 var json_data = data.message.comment_data;
83
84 if (window.commentsController !== undefined) {
85
86 window.commentsController.attachComment(json_data)
87 }
82 88 }
83 89 }
84 90
85 91 faviconUpdate(data) {
86 92 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
87 93 }
88 94
89 95 /** opens connection to ws server */
90 96 kickoffChannelstreamPlugin(data) {
91 97 ccLog.debug('kickoffChannelstreamPlugin');
92 98 var channels = ['broadcast'];
93 99 var addChannels = this.checkViewChannels();
94 100 for (var i = 0; i < addChannels.length; i++) {
95 101 channels.push(addChannels[i]);
96 102 }
97 103 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled) {
98 104 var channelstreamConnection = this.getChannelStreamConnection();
99 105 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
100 106 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
101 107 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
102 108 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
103 109 // some channels might already be registered by topic
104 110 for (var i = 0; i < channels.length; i++) {
105 111 channelstreamConnection.push('channels', channels[i]);
106 112 }
107 113 // append any additional channels registered in other plugins
108 114 $.Topic('/connection_controller/subscribe').processPrepared();
109 115
110 116 channelstreamConnection.connect();
111 117 }
112 118 }
113 119
114 120 checkViewChannels() {
115 121 // subscribe to different channels data is sent.
116 122
117 123 var channels = [];
118 124 // subscribe to PR repo channel for PR's'
119 125 if (templateContext.pull_request_data.pull_request_id) {
120 126 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
121 127 String(templateContext.pull_request_data.pull_request_id);
122 128 channels.push(channelName);
123 129 }
124 130
125 131 if (templateContext.commit_data.commit_id) {
126 132 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
127 133 String(templateContext.commit_data.commit_id);
128 134 channels.push(channelName);
129 135 }
130 136
131 137 return channels;
132 138 }
133 139
134 140 /** subscribes users from channels in channelstream */
135 141 subscribeToChannelTopic(channels) {
136 142 var channelstreamConnection = this.getChannelStreamConnection();
137 143 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
138 144 ccLog.debug('subscribeToChannelTopic', toSubscribe);
139 145 if (toSubscribe.length > 0) {
140 146 // if we are connected then subscribe
141 147 if (channelstreamConnection.connected) {
142 148 channelstreamConnection.subscribe(toSubscribe);
143 149 }
144 150 // not connected? just push channels onto the stack
145 151 else {
146 152 for (var i = 0; i < toSubscribe.length; i++) {
147 153 channelstreamConnection.push('channels', toSubscribe[i]);
148 154 }
149 155 }
150 156 }
151 157 }
152 158
153 159 /** publish received messages into correct topic */
154 160 receivedMessage(event) {
155 161 for (var i = 0; i < event.detail.length; i++) {
156 162 var message = event.detail[i];
157 163 if (message.message.topic) {
158 164 ccLog.debug('publishing', message.message.topic);
159 165 $.Topic(message.message.topic).publish(message);
160 166 }
161 167 else if (message.type === 'presence') {
162 168 $.Topic('/connection_controller/presence').publish(message);
163 169 }
164 170 else {
165 171 ccLog.warn('unhandled message', message);
166 172 }
167 173 }
168 174 }
169 175
170 176 handleConnected(event) {
171 177 var channelstreamConnection = this.getChannelStreamConnection();
172 178 channelstreamConnection.set('channelsState', event.detail.channels_info);
173 179 channelstreamConnection.set('userState', event.detail.state);
174 180 channelstreamConnection.set('channels', event.detail.channels);
175 181 this.propagageChannelsState();
176 182 }
177 183
178 184 handleSubscribed(event) {
179 185 var channelstreamConnection = this.getChannelStreamConnection();
180 186 var channelInfo = event.detail.channels_info;
181 187 var channelKeys = Object.keys(event.detail.channels_info);
182 188 for (var i = 0; i < channelKeys.length; i++) {
183 189 var key = channelKeys[i];
184 190 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
185 191 }
186 192 channelstreamConnection.set('channels', event.detail.channels);
187 193 this.propagageChannelsState();
188 194 }
189 195
190 196 /** propagates channel states on topics */
191 197 propagageChannelsState(event) {
192 198 var channelstreamConnection = this.getChannelStreamConnection();
193 199 var channel_data = channelstreamConnection.channelsState;
194 200 var channels = channelstreamConnection.channels;
195 201 for (var i = 0; i < channels.length; i++) {
196 202 var key = channels[i];
197 203 $.Topic('/connection_controller/channel_update').publish(
198 204 {channel: key, state: channel_data[key]}
199 205 );
200 206 }
201 207 }
202 208
203 209 }
204 210
205 211 customElements.define(RhodecodeApp.is, RhodecodeApp);
@@ -1,1349 +1,1450 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84 84
85 85 if (!(this instanceof CommentForm)) {
86 86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 87 }
88 88
89 89 // bind the element instance to our Form
90 90 $(formElement).get(0).CommentForm = this;
91 91
92 92 this.withLineNo = function(selector) {
93 93 var lineNo = this.lineNo;
94 94 if (lineNo === undefined) {
95 95 return selector
96 96 } else {
97 97 return selector + '_' + lineNo;
98 98 }
99 99 };
100 100
101 101 this.commitId = commitId;
102 102 this.pullRequestId = pullRequestId;
103 103 this.lineNo = lineNo;
104 104 this.initAutocompleteActions = initAutocompleteActions;
105 105
106 106 this.previewButton = this.withLineNo('#preview-btn');
107 107 this.previewContainer = this.withLineNo('#preview-container');
108 108
109 109 this.previewBoxSelector = this.withLineNo('#preview-box');
110 110
111 111 this.editButton = this.withLineNo('#edit-btn');
112 112 this.editContainer = this.withLineNo('#edit-container');
113 113 this.cancelButton = this.withLineNo('#cancel-btn');
114 114 this.commentType = this.withLineNo('#comment_type');
115 115
116 116 this.resolvesId = null;
117 117 this.resolvesActionId = null;
118 118
119 119 this.closesPr = '#close_pull_request';
120 120
121 121 this.cmBox = this.withLineNo('#text');
122 122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123 123
124 124 this.statusChange = this.withLineNo('#change_status');
125 125
126 126 this.submitForm = formElement;
127 127
128 128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 129 this.submitButtonText = this.submitButton.val();
130 130
131 131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 132 this.submitDraftButtonText = this.submitDraftButton.val();
133 133
134 134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 135 {'repo_name': templateContext.repo_name,
136 136 'commit_id': templateContext.commit_data.commit_id});
137 137
138 138 if (edit){
139 139 this.submitDraftButton.hide();
140 140 this.submitButtonText = _gettext('Update Comment');
141 141 $(this.commentType).prop('disabled', true);
142 142 $(this.commentType).addClass('disabled');
143 143 var editInfo =
144 144 '';
145 145 $(editInfo).insertBefore($(this.editButton).parent());
146 146 }
147 147
148 148 if (resolvesCommentId){
149 149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 151 $(this.commentType).prop('disabled', true);
152 152 $(this.commentType).addClass('disabled');
153 153
154 154 // disable select
155 155 setTimeout(function() {
156 156 $(self.statusChange).select2('readonly', true);
157 157 }, 10);
158 158
159 159 var resolvedInfo = (
160 160 '<li class="resolve-action">' +
161 161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 163 '</li>'
164 164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 166 }
167 167
168 168 // based on commitId, or pullRequestId decide where do we submit
169 169 // out data
170 170 if (this.commitId){
171 171 var pyurl = 'repo_commit_comment_create';
172 172 if(edit){
173 173 pyurl = 'repo_commit_comment_edit';
174 174 }
175 175 this.submitUrl = pyroutes.url(pyurl,
176 176 {'repo_name': templateContext.repo_name,
177 177 'commit_id': this.commitId,
178 178 'comment_id': comment_id});
179 179 this.selfUrl = pyroutes.url('repo_commit',
180 180 {'repo_name': templateContext.repo_name,
181 181 'commit_id': this.commitId});
182 182
183 183 } else if (this.pullRequestId) {
184 184 var pyurl = 'pullrequest_comment_create';
185 185 if(edit){
186 186 pyurl = 'pullrequest_comment_edit';
187 187 }
188 188 this.submitUrl = pyroutes.url(pyurl,
189 189 {'repo_name': templateContext.repo_name,
190 190 'pull_request_id': this.pullRequestId,
191 191 'comment_id': comment_id});
192 192 this.selfUrl = pyroutes.url('pullrequest_show',
193 193 {'repo_name': templateContext.repo_name,
194 194 'pull_request_id': this.pullRequestId});
195 195
196 196 } else {
197 197 throw new Error(
198 198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 199 }
200 200
201 201 // FUNCTIONS and helpers
202 202 var self = this;
203 203
204 204 this.isInline = function(){
205 205 return this.lineNo && this.lineNo != 'general';
206 206 };
207 207
208 208 this.getCmInstance = function(){
209 209 return this.cm
210 210 };
211 211
212 212 this.setPlaceholder = function(placeholder) {
213 213 var cm = this.getCmInstance();
214 214 if (cm){
215 215 cm.setOption('placeholder', placeholder);
216 216 }
217 217 };
218 218
219 219 this.getCommentStatus = function() {
220 220 return $(this.submitForm).find(this.statusChange).val();
221 221 };
222 222
223 223 this.getCommentType = function() {
224 224 return $(this.submitForm).find(this.commentType).val();
225 225 };
226 226
227 227 this.getDraftState = function () {
228 228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 229 var data = $(submitterElem).data('isDraft');
230 230 return data
231 231 }
232 232
233 233 this.getResolvesId = function() {
234 234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 235 };
236 236
237 237 this.getClosePr = function() {
238 238 return $(this.submitForm).find(this.closesPr).val() || null;
239 239 };
240 240
241 241 this.markCommentResolved = function(resolvedCommentId){
242 242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 244 };
245 245
246 246 this.isAllowedToSubmit = function() {
247 247 var commentDisabled = $(this.submitButton).prop('disabled');
248 248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 249 return !commentDisabled && !draftDisabled;
250 250 };
251 251
252 252 this.initStatusChangeSelector = function(){
253 253 var formatChangeStatus = function(state, escapeMarkup) {
254 254 var originalOption = state.element;
255 255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 256 return tmpl
257 257 };
258 258 var formatResult = function(result, container, query, escapeMarkup) {
259 259 return formatChangeStatus(result, escapeMarkup);
260 260 };
261 261
262 262 var formatSelection = function(data, container, escapeMarkup) {
263 263 return formatChangeStatus(data, escapeMarkup);
264 264 };
265 265
266 266 $(this.submitForm).find(this.statusChange).select2({
267 267 placeholder: _gettext('Status Review'),
268 268 formatResult: formatResult,
269 269 formatSelection: formatSelection,
270 270 containerCssClass: "drop-menu status_box_menu",
271 271 dropdownCssClass: "drop-menu-dropdown",
272 272 dropdownAutoWidth: true,
273 273 minimumResultsForSearch: -1
274 274 });
275 275
276 276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 277 var status = self.getCommentStatus();
278 278
279 279 if (status && !self.isInline()) {
280 280 $(self.submitButton).prop('disabled', false);
281 281 $(self.submitDraftButton).prop('disabled', false);
282 282 }
283 283
284 284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 285 self.setPlaceholder(placeholderText)
286 286 })
287 287 };
288 288
289 289 // reset the comment form into it's original state
290 290 this.resetCommentFormState = function(content) {
291 291 content = content || '';
292 292
293 293 $(this.editContainer).show();
294 294 $(this.editButton).parent().addClass('active');
295 295
296 296 $(this.previewContainer).hide();
297 297 $(this.previewButton).parent().removeClass('active');
298 298
299 299 this.setActionButtonsDisabled(true);
300 300 self.cm.setValue(content);
301 301 self.cm.setOption("readOnly", false);
302 302
303 303 if (this.resolvesId) {
304 304 // destroy the resolve action
305 305 $(this.resolvesId).parent().remove();
306 306 }
307 307 // reset closingPR flag
308 308 $('.close-pr-input').remove();
309 309
310 310 $(this.statusChange).select2('readonly', false);
311 311 };
312 312
313 313 this.globalSubmitSuccessCallback = function(comment){
314 314 // default behaviour is to call GLOBAL hook, if it's registered.
315 315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 316 commentFormGlobalSubmitSuccessCallback(comment);
317 317 }
318 318 };
319 319
320 320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 322 };
323 323
324 324 // overwrite a submitHandler, we need to do it for inline comments
325 325 this.setHandleFormSubmit = function(callback) {
326 326 this.handleFormSubmit = callback;
327 327 };
328 328
329 329 // overwrite a submitSuccessHandler
330 330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 331 this.globalSubmitSuccessCallback = callback;
332 332 };
333 333
334 334 // default handler for for submit for main comments
335 335 this.handleFormSubmit = function() {
336 336 var text = self.cm.getValue();
337 337 var status = self.getCommentStatus();
338 338 var commentType = self.getCommentType();
339 339 var isDraft = self.getDraftState();
340 340 var resolvesCommentId = self.getResolvesId();
341 341 var closePullRequest = self.getClosePr();
342 342
343 343 if (text === "" && !status) {
344 344 return;
345 345 }
346 346
347 347 var excludeCancelBtn = false;
348 348 var submitEvent = true;
349 349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 350 self.cm.setOption("readOnly", true);
351 351
352 352 var postData = {
353 353 'text': text,
354 354 'changeset_status': status,
355 355 'comment_type': commentType,
356 356 'csrf_token': CSRF_TOKEN
357 357 };
358 358
359 359 if (resolvesCommentId) {
360 360 postData['resolves_comment_id'] = resolvesCommentId;
361 361 }
362 362
363 363 if (closePullRequest) {
364 364 postData['close_pull_request'] = true;
365 365 }
366 366
367 var submitSuccessCallback = function(o) {
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
368 369 // reload page if we change status for single commit.
369 370 if (status && self.commitId) {
370 371 location.reload(true);
371 372 } else {
372 $('#injected_page_comments').append(o.rendered_text);
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 self.attachGeneralComment(json_data)
375
373 376 self.resetCommentFormState();
374 377 timeagoActivate();
375 378 tooltipActivate();
376 379
377 380 // mark visually which comment was resolved
378 381 if (resolvesCommentId) {
379 382 self.markCommentResolved(resolvesCommentId);
380 383 }
381 384 }
382 385
383 386 // run global callback on submit
384 387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
385 388
386 389 };
387 390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
388 391 var prefix = "Error while submitting comment.\n"
389 392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
390 393 ajaxErrorSwal(message);
391 394 self.resetCommentFormState(text);
392 395 };
393 396 self.submitAjaxPOST(
394 397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
395 398 };
396 399
397 400 this.previewSuccessCallback = function(o) {
398 401 $(self.previewBoxSelector).html(o);
399 402 $(self.previewBoxSelector).removeClass('unloaded');
400 403
401 404 // swap buttons, making preview active
402 405 $(self.previewButton).parent().addClass('active');
403 406 $(self.editButton).parent().removeClass('active');
404 407
405 408 // unlock buttons
406 409 self.setActionButtonsDisabled(false);
407 410 };
408 411
409 412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
410 413 excludeCancelBtn = excludeCancelBtn || false;
411 414 submitEvent = submitEvent || false;
412 415
413 416 $(this.editButton).prop('disabled', state);
414 417 $(this.previewButton).prop('disabled', state);
415 418
416 419 if (!excludeCancelBtn) {
417 420 $(this.cancelButton).prop('disabled', state);
418 421 }
419 422
420 423 var submitState = state;
421 424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
422 425 // if the value of commit review status is set, we allow
423 426 // submit button, but only on Main form, isInline means inline
424 427 submitState = false
425 428 }
426 429
427 430 $(this.submitButton).prop('disabled', submitState);
428 431 $(this.submitDraftButton).prop('disabled', submitState);
429 432
430 433 if (submitEvent) {
431 434 var isDraft = self.getDraftState();
432 435
433 436 if (isDraft) {
434 437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
435 438 } else {
436 439 $(this.submitButton).val(_gettext('Submitting...'));
437 440 }
438 441
439 442 } else {
440 443 $(this.submitButton).val(this.submitButtonText);
441 444 $(this.submitDraftButton).val(this.submitDraftButtonText);
442 445 }
443 446
444 447 };
445 448
446 449 // lock preview/edit/submit buttons on load, but exclude cancel button
447 450 var excludeCancelBtn = true;
448 451 this.setActionButtonsDisabled(true, excludeCancelBtn);
449 452
450 453 // anonymous users don't have access to initialized CM instance
451 454 if (this.cm !== undefined){
452 455 this.cm.on('change', function(cMirror) {
453 456 if (cMirror.getValue() === "") {
454 457 self.setActionButtonsDisabled(true, excludeCancelBtn)
455 458 } else {
456 459 self.setActionButtonsDisabled(false, excludeCancelBtn)
457 460 }
458 461 });
459 462 }
460 463
461 464 $(this.editButton).on('click', function(e) {
462 465 e.preventDefault();
463 466
464 467 $(self.previewButton).parent().removeClass('active');
465 468 $(self.previewContainer).hide();
466 469
467 470 $(self.editButton).parent().addClass('active');
468 471 $(self.editContainer).show();
469 472
470 473 });
471 474
472 475 $(this.previewButton).on('click', function(e) {
473 476 e.preventDefault();
474 477 var text = self.cm.getValue();
475 478
476 479 if (text === "") {
477 480 return;
478 481 }
479 482
480 483 var postData = {
481 484 'text': text,
482 485 'renderer': templateContext.visual.default_renderer,
483 486 'csrf_token': CSRF_TOKEN
484 487 };
485 488
486 489 // lock ALL buttons on preview
487 490 self.setActionButtonsDisabled(true);
488 491
489 492 $(self.previewBoxSelector).addClass('unloaded');
490 493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
491 494
492 495 $(self.editContainer).hide();
493 496 $(self.previewContainer).show();
494 497
495 498 // by default we reset state of comment preserving the text
496 499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
497 500 var prefix = "Error while preview of comment.\n"
498 501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
499 502 ajaxErrorSwal(message);
500 503
501 504 self.resetCommentFormState(text)
502 505 };
503 506 self.submitAjaxPOST(
504 507 self.previewUrl, postData, self.previewSuccessCallback,
505 508 previewFailCallback);
506 509
507 510 $(self.previewButton).parent().addClass('active');
508 511 $(self.editButton).parent().removeClass('active');
509 512 });
510 513
511 514 $(this.submitForm).submit(function(e) {
512 515 e.preventDefault();
513 516 var allowedToSubmit = self.isAllowedToSubmit();
514 517 if (!allowedToSubmit){
515 518 return false;
516 519 }
517 520
518 521 self.handleFormSubmit();
519 522 });
520 523
521 524 }
522 525
523 526 return CommentForm;
524 527 });
525 528
526 529 /* selector for comment versions */
527 530 var initVersionSelector = function(selector, initialData) {
528 531
529 532 var formatResult = function(result, container, query, escapeMarkup) {
530 533
531 534 return renderTemplate('commentVersion', {
532 535 show_disabled: true,
533 536 version: result.comment_version,
534 537 user_name: result.comment_author_username,
535 538 gravatar_url: result.comment_author_gravatar,
536 539 size: 16,
537 540 timeago_component: result.comment_created_on,
538 541 })
539 542 };
540 543
541 544 $(selector).select2({
542 545 placeholder: "Edited",
543 546 containerCssClass: "drop-menu-comment-history",
544 547 dropdownCssClass: "drop-menu-dropdown",
545 548 dropdownAutoWidth: true,
546 549 minimumResultsForSearch: -1,
547 550 data: initialData,
548 551 formatResult: formatResult,
549 552 });
550 553
551 554 $(selector).on('select2-selecting', function (e) {
552 555 // hide the mast as we later do preventDefault()
553 556 $("#select2-drop-mask").click();
554 557 e.preventDefault();
555 558 e.choice.action();
556 559 });
557 560
558 561 $(selector).on("select2-open", function() {
559 562 timeagoActivate();
560 563 });
561 564 };
562 565
563 566 /* comments controller */
564 567 var CommentsController = function() {
565 568 var mainComment = '#text';
566 569 var self = this;
567 570
568 this.cancelComment = function (node) {
569 var $node = $(node);
570 var edit = $(this).attr('edit');
571 if (edit) {
572 var $general_comments = null;
573 var $inline_comments = $node.closest('div.inline-comments');
574 if (!$inline_comments.length) {
575 $general_comments = $('#comments');
576 var $comment = $general_comments.parent().find('div.comment:hidden');
577 // show hidden general comment form
578 $('#cb-comment-general-form-placeholder').show();
579 } else {
580 var $comment = $inline_comments.find('div.comment:hidden');
581 }
582 $comment.show();
583 }
584 $node.closest('.comment-inline-form').remove();
585 return false;
586 };
587
588 571 this.showVersion = function (comment_id, comment_history_id) {
589 572
590 573 var historyViewUrl = pyroutes.url(
591 574 'repo_commit_comment_history_view',
592 575 {
593 576 'repo_name': templateContext.repo_name,
594 577 'commit_id': comment_id,
595 578 'comment_history_id': comment_history_id,
596 579 }
597 580 );
598 581 successRenderCommit = function (data) {
599 582 SwalNoAnimation.fire({
600 583 html: data,
601 584 title: '',
602 585 });
603 586 };
604 587 failRenderCommit = function () {
605 588 SwalNoAnimation.fire({
606 589 html: 'Error while loading comment history',
607 590 title: '',
608 591 });
609 592 };
610 593 _submitAjaxPOST(
611 594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
612 595 successRenderCommit,
613 596 failRenderCommit
614 597 );
615 598 };
616 599
617 600 this.getLineNumber = function(node) {
618 601 var $node = $(node);
619 602 var lineNo = $node.closest('td').attr('data-line-no');
620 603 if (lineNo === undefined && $node.data('commentInline')){
621 604 lineNo = $node.data('commentLineNo')
622 605 }
623 606
624 607 return lineNo
625 608 };
626 609
627 610 this.scrollToComment = function(node, offset, outdated) {
628 611 if (offset === undefined) {
629 612 offset = 0;
630 613 }
631 614 var outdated = outdated || false;
632 615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
633 616
634 617 if (!node) {
635 618 node = $('.comment-selected');
636 619 if (!node.length) {
637 620 node = $('comment-current')
638 621 }
639 622 }
640 623
641 624 $wrapper = $(node).closest('div.comment');
642 625
643 626 // show hidden comment when referenced.
644 627 if (!$wrapper.is(':visible')){
645 628 $wrapper.show();
646 629 }
647 630
648 631 $comment = $(node).closest(klass);
649 632 $comments = $(klass);
650 633
651 634 $('.comment-selected').removeClass('comment-selected');
652 635
653 636 var nextIdx = $(klass).index($comment) + offset;
654 637 if (nextIdx >= $comments.length) {
655 638 nextIdx = 0;
656 639 }
657 640 var $next = $(klass).eq(nextIdx);
658 641
659 642 var $cb = $next.closest('.cb');
660 643 $cb.removeClass('cb-collapsed');
661 644
662 645 var $filediffCollapseState = $cb.closest('.filediff').prev();
663 646 $filediffCollapseState.prop('checked', false);
664 647 $next.addClass('comment-selected');
665 648 scrollToElement($next);
666 649 return false;
667 650 };
668 651
669 652 this.nextComment = function(node) {
670 653 return self.scrollToComment(node, 1);
671 654 };
672 655
673 656 this.prevComment = function(node) {
674 657 return self.scrollToComment(node, -1);
675 658 };
676 659
677 660 this.nextOutdatedComment = function(node) {
678 661 return self.scrollToComment(node, 1, true);
679 662 };
680 663
681 664 this.prevOutdatedComment = function(node) {
682 665 return self.scrollToComment(node, -1, true);
683 666 };
684 667
668 this.cancelComment = function (node) {
669 var $node = $(node);
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
672
673 if (edit) {
674 var $general_comments = null;
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
683 $comment.show();
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
691 }
692
693 $node.closest('.comment-inline-form').remove();
694 return false;
695 };
696
685 697 this._deleteComment = function(node) {
686 698 var $node = $(node);
687 699 var $td = $node.closest('td');
688 700 var $comment = $node.closest('.comment');
689 701 var comment_id = $($comment).data('commentId');
690 702 var isDraft = $($comment).data('commentDraft');
691 703 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
692 704 var postData = {
693 705 'csrf_token': CSRF_TOKEN
694 706 };
695 707
696 708 $comment.addClass('comment-deleting');
697 709 $comment.hide('fast');
698 710
699 711 var success = function(response) {
700 712 $comment.remove();
701 713
702 714 if (window.updateSticky !== undefined) {
703 715 // potentially our comments change the active window size, so we
704 716 // notify sticky elements
705 717 updateSticky()
706 718 }
707 719
708 720 if (window.refreshAllComments !== undefined && !isDraft) {
709 721 // if we have this handler, run it, and refresh all comments boxes
710 722 refreshAllComments()
711 723 }
712 724 return false;
713 725 };
714 726
715 727 var failure = function(jqXHR, textStatus, errorThrown) {
716 728 var prefix = "Error while deleting this comment.\n"
717 729 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
718 730 ajaxErrorSwal(message);
719 731
720 732 $comment.show('fast');
721 733 $comment.removeClass('comment-deleting');
722 734 return false;
723 735 };
724 736 ajaxPOST(url, postData, success, failure);
725 737
726 738
727 739
728 740 }
729 741
730 742 this.deleteComment = function(node) {
731 743 var $comment = $(node).closest('.comment');
732 744 var comment_id = $comment.attr('data-comment-id');
733 745
734 746 SwalNoAnimation.fire({
735 747 title: 'Delete this comment?',
736 748 icon: 'warning',
737 749 showCancelButton: true,
738 750 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
739 751
740 752 }).then(function(result) {
741 753 if (result.value) {
742 754 self._deleteComment(node);
743 755 }
744 756 })
745 757 };
746 758
747 759 this._finalizeDrafts = function(commentIds) {
748 760 window.finalizeDrafts(commentIds)
749 761 }
750 762
751 763 this.finalizeDrafts = function(commentIds) {
752 764
753 765 SwalNoAnimation.fire({
754 title: _ngettext('Submit {0} draft comment', 'Submit {0} draft comments', commentIds.length).format(commentIds.length),
766 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
755 767 icon: 'warning',
756 768 showCancelButton: true,
757 769 confirmButtonText: _gettext('Yes, finalize drafts'),
758 770
759 771 }).then(function(result) {
760 772 if (result.value) {
761 773 self._finalizeDrafts(commentIds);
762 774 }
763 775 })
764 776 };
765 777
766 778 this.toggleWideMode = function (node) {
779
767 780 if ($('#content').hasClass('wrapper')) {
768 781 $('#content').removeClass("wrapper");
769 782 $('#content').addClass("wide-mode-wrapper");
770 783 $(node).addClass('btn-success');
771 784 return true
772 785 } else {
773 786 $('#content').removeClass("wide-mode-wrapper");
774 787 $('#content').addClass("wrapper");
775 788 $(node).removeClass('btn-success');
776 789 return false
777 790 }
778 791
779 792 };
780 793
781 this.toggleComments = function(node, show) {
794 /**
795 * Turn off/on all comments in file diff
796 */
797 this.toggleDiffComments = function(node) {
798 // Find closes filediff container
782 799 var $filediff = $(node).closest('.filediff');
800 if ($(node).hasClass('toggle-on')) {
801 var show = false;
802 } else if ($(node).hasClass('toggle-off')) {
803 var show = true;
804 }
805
806 // Toggle each individual comment block, so we can un-toggle single ones
807 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
808 self.toggleLineComments($(val), show)
809 })
810
811 // since we change the height of the diff container that has anchor points for upper
812 // sticky header, we need to tell it to re-calculate those
813 if (window.updateSticky !== undefined) {
814 // potentially our comments change the active window size, so we
815 // notify sticky elements
816 updateSticky()
817 }
818
819 return false;
820 }
821
822 this.toggleLineComments = function(node, show) {
823
824 var trElem = $(node).closest('tr')
825
783 826 if (show === true) {
784 $filediff.removeClass('hide-comments');
827 // mark outdated comments as visible before the toggle;
828 $(trElem).find('.comment-outdated').show();
829 $(trElem).removeClass('hide-line-comments');
785 830 } else if (show === false) {
786 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
787 $filediff.addClass('hide-comments');
831 $(trElem).find('.comment-outdated').hide();
832 $(trElem).addClass('hide-line-comments');
788 833 } else {
789 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
790 $filediff.toggleClass('hide-comments');
834 // mark outdated comments as visible before the toggle;
835 $(trElem).find('.comment-outdated').show();
836 $(trElem).toggleClass('hide-line-comments');
791 837 }
792 838
793 839 // since we change the height of the diff container that has anchor points for upper
794 840 // sticky header, we need to tell it to re-calculate those
795 841 if (window.updateSticky !== undefined) {
796 842 // potentially our comments change the active window size, so we
797 843 // notify sticky elements
798 844 updateSticky()
799 845 }
800 846
801 return false;
802 };
803
804 this.toggleLineComments = function(node) {
805 self.toggleComments(node, true);
806 var $node = $(node);
807 // mark outdated comments as visible before the toggle;
808 $(node.closest('tr')).find('.comment-outdated').show();
809 $node.closest('tr').toggleClass('hide-line-comments');
810 847 };
811 848
812 849 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
813 850 var pullRequestId = templateContext.pull_request_data.pull_request_id;
814 851 var commitId = templateContext.commit_data.commit_id;
815 852
816 853 var commentForm = new CommentForm(
817 854 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
818 855 var cm = commentForm.getCmInstance();
819 856
820 857 if (resolvesCommentId){
821 858 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
822 859 }
823 860
824 861 setTimeout(function() {
825 862 // callbacks
826 863 if (cm !== undefined) {
827 864 commentForm.setPlaceholder(placeholderText);
828 865 if (commentForm.isInline()) {
829 866 cm.focus();
830 867 cm.refresh();
831 868 }
832 869 }
833 870 }, 10);
834 871
835 872 // trigger scrolldown to the resolve comment, since it might be away
836 873 // from the clicked
837 874 if (resolvesCommentId){
838 875 var actionNode = $(commentForm.resolvesActionId).offset();
839 876
840 877 setTimeout(function() {
841 878 if (actionNode) {
842 879 $('body, html').animate({scrollTop: actionNode.top}, 10);
843 880 }
844 881 }, 100);
845 882 }
846 883
847 884 // add dropzone support
848 885 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
849 886 var renderer = templateContext.visual.default_renderer;
850 887 if (renderer == 'rst') {
851 888 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
852 889 if (isRendered){
853 890 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
854 891 }
855 892 } else if (renderer == 'markdown') {
856 893 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
857 894 if (isRendered){
858 895 attachmentUrl = '!' + attachmentUrl;
859 896 }
860 897 } else {
861 898 var attachmentUrl = '{}'.format(attachmentStoreUrl);
862 899 }
863 900 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
864 901
865 902 return false;
866 903 };
867 904
868 905 //see: https://www.dropzonejs.com/#configuration
869 906 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
870 907 {'repo_name': templateContext.repo_name,
871 908 'commit_id': templateContext.commit_data.commit_id})
872 909
873 910 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
874 911 if (previewTmpl !== undefined){
875 912 var selectLink = $(formElement).find('.pick-attachment').get(0);
876 913 $(formElement).find('.comment-attachment-uploader').dropzone({
877 914 url: storeUrl,
878 915 headers: {"X-CSRF-Token": CSRF_TOKEN},
879 916 paramName: function () {
880 917 return "attachment"
881 918 }, // The name that will be used to transfer the file
882 919 clickable: selectLink,
883 920 parallelUploads: 1,
884 921 maxFiles: 10,
885 922 maxFilesize: templateContext.attachment_store.max_file_size_mb,
886 923 uploadMultiple: false,
887 924 autoProcessQueue: true, // if false queue will not be processed automatically.
888 925 createImageThumbnails: false,
889 926 previewTemplate: previewTmpl.innerHTML,
890 927
891 928 accept: function (file, done) {
892 929 done();
893 930 },
894 931 init: function () {
895 932
896 933 this.on("sending", function (file, xhr, formData) {
897 934 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
898 935 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
899 936 });
900 937
901 938 this.on("success", function (file, response) {
902 939 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
903 940 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
904 941
905 942 var isRendered = false;
906 943 var ext = file.name.split('.').pop();
907 944 var imageExts = templateContext.attachment_store.image_ext;
908 945 if (imageExts.indexOf(ext) !== -1){
909 946 isRendered = true;
910 947 }
911 948
912 949 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
913 950 });
914 951
915 952 this.on("error", function (file, errorMessage, xhr) {
916 953 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
917 954
918 955 var error = null;
919 956
920 957 if (xhr !== undefined){
921 958 var httpStatus = xhr.status + " " + xhr.statusText;
922 959 if (xhr !== undefined && xhr.status >= 500) {
923 960 error = httpStatus;
924 961 }
925 962 }
926 963
927 964 if (error === null) {
928 965 error = errorMessage.error || errorMessage || httpStatus;
929 966 }
930 967 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
931 968
932 969 });
933 970 }
934 971 });
935 972 }
936 973 return commentForm;
937 974 };
938 975
939 976 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
940 977
941 978 var tmpl = $('#cb-comment-general-form-template').html();
942 979 tmpl = tmpl.format(null, 'general');
943 980 var $form = $(tmpl);
944 981
945 982 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
946 983 var curForm = $formPlaceholder.find('form');
947 984 if (curForm){
948 985 curForm.remove();
949 986 }
950 987 $formPlaceholder.append($form);
951 988
952 989 var _form = $($form[0]);
953 990 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
954 991 var edit = false;
955 992 var comment_id = null;
956 993 var commentForm = this.createCommentForm(
957 994 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
958 995 commentForm.initStatusChangeSelector();
959 996
960 997 return commentForm;
961 998 };
962 999
963 this.editComment = function(node) {
1000 this.editComment = function(node, line_no, f_path) {
1001 self.edit = true;
964 1002 var $node = $(node);
1003 var $td = $node.closest('td');
1004
965 1005 var $comment = $(node).closest('.comment');
966 1006 var comment_id = $($comment).data('commentId');
967 1007 var isDraft = $($comment).data('commentDraft');
968 var $form = null
1008 var $editForm = null
969 1009
970 1010 var $comments = $node.closest('div.inline-comments');
971 1011 var $general_comments = null;
972 var lineno = null;
973 1012
974 1013 if($comments.length){
975 1014 // inline comments setup
976 $form = $comments.find('.comment-inline-form');
977 lineno = self.getLineNumber(node)
1015 $editForm = $comments.find('.comment-inline-form');
1016 line_no = self.getLineNumber(node)
978 1017 }
979 1018 else{
980 1019 // general comments setup
981 1020 $comments = $('#comments');
982 $form = $comments.find('.comment-inline-form');
983 lineno = $comment[0].id
1021 $editForm = $comments.find('.comment-inline-form');
1022 line_no = $comment[0].id
984 1023 $('#cb-comment-general-form-placeholder').hide();
985 1024 }
986 1025
987 this.edit = true;
1026 if ($editForm.length === 0) {
988 1027
989 if (!$form.length) {
990
1028 // unhide all comments if they are hidden for a proper REPLY mode
991 1029 var $filediff = $node.closest('.filediff');
992 1030 $filediff.removeClass('hide-comments');
993 var f_path = $filediff.attr('data-f-path');
994
995 // create a new HTML from template
996 1031
997 var tmpl = $('#cb-comment-inline-form-template').html();
998 tmpl = tmpl.format(escapeHtml(f_path), lineno);
999 $form = $(tmpl);
1000 $comment.after($form)
1032 $editForm = self.createNewFormWrapper(f_path, line_no);
1033 if(f_path && line_no) {
1034 $editForm.addClass('comment-inline-form-edit')
1035 }
1001 1036
1002 var _form = $($form[0]).find('form');
1037 $comment.after($editForm)
1038
1039 var _form = $($editForm[0]).find('form');
1003 1040 var autocompleteActions = ['as_note',];
1004 1041 var commentForm = this.createCommentForm(
1005 _form, lineno, '', autocompleteActions, resolvesCommentId,
1042 _form, line_no, '', autocompleteActions, resolvesCommentId,
1006 1043 this.edit, comment_id);
1007 1044 var old_comment_text_binary = $comment.attr('data-comment-text');
1008 1045 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1009 1046 commentForm.cm.setValue(old_comment_text);
1010 1047 $comment.hide();
1048 tooltipActivate();
1011 1049
1012 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1013 form: _form,
1014 parent: $comments,
1015 lineno: lineno,
1016 f_path: f_path}
1017 );
1018
1019 // set a CUSTOM submit handler for inline comments.
1020 commentForm.setHandleFormSubmit(function(o) {
1050 // set a CUSTOM submit handler for inline comment edit action.
1051 commentForm.setHandleFormSubmit(function(o) {
1021 1052 var text = commentForm.cm.getValue();
1022 1053 var commentType = commentForm.getCommentType();
1023 1054
1024 1055 if (text === "") {
1025 1056 return;
1026 1057 }
1027 1058
1028 1059 if (old_comment_text == text) {
1029 1060 SwalNoAnimation.fire({
1030 1061 title: 'Unable to edit comment',
1031 1062 html: _gettext('Comment body was not changed.'),
1032 1063 });
1033 1064 return;
1034 1065 }
1035 1066 var excludeCancelBtn = false;
1036 1067 var submitEvent = true;
1037 1068 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1038 1069 commentForm.cm.setOption("readOnly", true);
1039 1070
1040 1071 // Read last version known
1041 1072 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1042 1073 var version = versionSelector.data('lastVersion');
1043 1074
1044 1075 if (!version) {
1045 1076 version = 0;
1046 1077 }
1047 1078
1048 1079 var postData = {
1049 1080 'text': text,
1050 1081 'f_path': f_path,
1051 'line': lineno,
1082 'line': line_no,
1052 1083 'comment_type': commentType,
1053 1084 'draft': isDraft,
1054 1085 'version': version,
1055 1086 'csrf_token': CSRF_TOKEN
1056 1087 };
1057 1088
1058 1089 var submitSuccessCallback = function(json_data) {
1059 $form.remove();
1090 $editForm.remove();
1060 1091 $comment.show();
1061 1092 var postData = {
1062 1093 'text': text,
1063 1094 'renderer': $comment.attr('data-comment-renderer'),
1064 1095 'csrf_token': CSRF_TOKEN
1065 1096 };
1066 1097
1067 1098 /* Inject new edited version selector */
1068 1099 var updateCommentVersionDropDown = function () {
1069 1100 var versionSelectId = '#comment_versions_'+comment_id;
1070 1101 var preLoadVersionData = [
1071 1102 {
1072 1103 id: json_data['comment_version'],
1073 1104 text: "v{0}".format(json_data['comment_version']),
1074 1105 action: function () {
1075 1106 Rhodecode.comments.showVersion(
1076 1107 json_data['comment_id'],
1077 1108 json_data['comment_history_id']
1078 1109 )
1079 1110 },
1080 1111 comment_version: json_data['comment_version'],
1081 1112 comment_author_username: json_data['comment_author_username'],
1082 1113 comment_author_gravatar: json_data['comment_author_gravatar'],
1083 1114 comment_created_on: json_data['comment_created_on'],
1084 1115 },
1085 1116 ]
1086 1117
1087 1118
1088 1119 if ($(versionSelectId).data('select2')) {
1089 1120 var oldData = $(versionSelectId).data('select2').opts.data.results;
1090 1121 $(versionSelectId).select2("destroy");
1091 1122 preLoadVersionData = oldData.concat(preLoadVersionData)
1092 1123 }
1093 1124
1094 1125 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1095 1126
1096 1127 $comment.attr('data-comment-text', utf8ToB64(text));
1097 1128
1098 1129 var versionSelector = $('#comment_versions_'+comment_id);
1099 1130
1100 1131 // set lastVersion so we know our last edit version
1101 1132 versionSelector.data('lastVersion', json_data['comment_version'])
1102 1133 versionSelector.parent().show();
1103 1134 }
1104 1135 updateCommentVersionDropDown();
1105 1136
1106 1137 // by default we reset state of comment preserving the text
1107 1138 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1108 1139 var prefix = "Error while editing this comment.\n"
1109 1140 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1110 1141 ajaxErrorSwal(message);
1111 1142 };
1112 1143
1113 1144 var successRenderCommit = function(o){
1114 1145 $comment.show();
1115 1146 $comment[0].lastElementChild.innerHTML = o;
1116 1147 };
1117 1148
1118 1149 var previewUrl = pyroutes.url(
1119 1150 'repo_commit_comment_preview',
1120 1151 {'repo_name': templateContext.repo_name,
1121 1152 'commit_id': templateContext.commit_data.commit_id});
1122 1153
1123 1154 _submitAjaxPOST(
1124 previewUrl, postData, successRenderCommit,
1125 failRenderCommit
1155 previewUrl, postData, successRenderCommit, failRenderCommit
1126 1156 );
1127 1157
1128 1158 try {
1129 1159 var html = json_data.rendered_text;
1130 1160 var lineno = json_data.line_no;
1131 1161 var target_id = json_data.target_id;
1132 1162
1133 1163 $comments.find('.cb-comment-add-button').before(html);
1134 1164
1135 1165 // run global callback on submit
1136 1166 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1137 1167
1138 1168 } catch (e) {
1139 1169 console.error(e);
1140 1170 }
1141 1171
1142 1172 // re trigger the linkification of next/prev navigation
1143 1173 linkifyComments($('.inline-comment-injected'));
1144 1174 timeagoActivate();
1145 1175 tooltipActivate();
1146 1176
1147 1177 if (window.updateSticky !== undefined) {
1148 1178 // potentially our comments change the active window size, so we
1149 1179 // notify sticky elements
1150 1180 updateSticky()
1151 1181 }
1152 1182
1153 1183 if (window.refreshAllComments !== undefined && !isDraft) {
1154 1184 // if we have this handler, run it, and refresh all comments boxes
1155 1185 refreshAllComments()
1156 1186 }
1157 1187
1158 1188 commentForm.setActionButtonsDisabled(false);
1159 1189
1160 1190 };
1161 1191
1162 1192 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1163 1193 var prefix = "Error while editing comment.\n"
1164 1194 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1165 1195 if (jqXHR.status == 409){
1166 1196 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1167 1197 ajaxErrorSwal(message, 'Comment version mismatch.');
1168 1198 } else {
1169 1199 ajaxErrorSwal(message);
1170 1200 }
1171 1201
1172 1202 commentForm.resetCommentFormState(text)
1173 1203 };
1174 1204 commentForm.submitAjaxPOST(
1175 1205 commentForm.submitUrl, postData,
1176 1206 submitSuccessCallback,
1177 1207 submitFailCallback);
1178 1208 });
1179 1209 }
1180 1210
1181 $form.addClass('comment-inline-form-open');
1211 $editForm.addClass('comment-inline-form-open');
1182 1212 };
1183 1213
1184 this.createComment = function(node, resolutionComment) {
1185 var resolvesCommentId = resolutionComment || null;
1214 this.attachComment = function(json_data) {
1215 var self = this;
1216 $.each(json_data, function(idx, val) {
1217 var json_data_elem = [val]
1218 var isInline = val.comment_f_path && val.comment_lineno
1219
1220 if (isInline) {
1221 self.attachInlineComment(json_data_elem)
1222 } else {
1223 self.attachGeneralComment(json_data_elem)
1224 }
1225 })
1226
1227 }
1228
1229 this.attachGeneralComment = function(json_data) {
1230 $.each(json_data, function(idx, val) {
1231 $('#injected_page_comments').append(val.rendered_text);
1232 })
1233 }
1234
1235 this.attachInlineComment = function(json_data) {
1236
1237 $.each(json_data, function (idx, val) {
1238 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1239 var html = val.rendered_text;
1240 var $inlineComments = $('#' + val.target_id)
1241 .find(line_qry)
1242 .find('.inline-comments');
1243
1244 var lastComment = $inlineComments.find('.comment-inline').last();
1245
1246 if (lastComment.length === 0) {
1247 // first comment, we append simply
1248 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1249 } else {
1250 $(lastComment).after(html)
1251 }
1252
1253 })
1254
1255 };
1256
1257 this.createNewFormWrapper = function(f_path, line_no) {
1258 // create a new reply HTML form from template
1259 var tmpl = $('#cb-comment-inline-form-template').html();
1260 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1261 return $(tmpl);
1262 }
1263
1264 this.createComment = function(node, f_path, line_no, resolutionComment) {
1265 self.edit = false;
1186 1266 var $node = $(node);
1187 1267 var $td = $node.closest('td');
1188 var $form = $td.find('.comment-inline-form');
1189 this.edit = false;
1268 var resolvesCommentId = resolutionComment || null;
1190 1269
1191 if (!$form.length) {
1270 var $replyForm = $td.find('.comment-inline-form');
1192 1271
1193 var $filediff = $node.closest('.filediff');
1194 $filediff.removeClass('hide-comments');
1195 var f_path = $filediff.attr('data-f-path');
1196 var lineno = self.getLineNumber(node);
1197 // create a new HTML from template
1198 var tmpl = $('#cb-comment-inline-form-template').html();
1199 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1200 $form = $(tmpl);
1272 // if form isn't existing, we're generating a new one and injecting it.
1273 if ($replyForm.length === 0) {
1274
1275 // unhide/expand all comments if they are hidden for a proper REPLY mode
1276 self.toggleLineComments($node, true);
1277
1278 $replyForm = self.createNewFormWrapper(f_path, line_no);
1201 1279
1202 1280 var $comments = $td.find('.inline-comments');
1203 if (!$comments.length) {
1204 $comments = $(
1205 $('#cb-comments-inline-container-template').html());
1206 $td.append($comments);
1281
1282 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1283 if ($comments.length===0) {
1284 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1285 var $reply_container = $('#cb-comments-inline-container-template')
1286 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1287 $td.append($($reply_container).html());
1207 1288 }
1208 1289
1209 $td.find('.cb-comment-add-button').before($form);
1290 // default comment button exists, so we prepend the form for leaving initial comment
1291 $td.find('.cb-comment-add-button').before($replyForm);
1292 // set marker, that we have a open form
1293 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1294 $replyWrapper.addClass('comment-form-active');
1210 1295
1211 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1212 var _form = $($form[0]).find('form');
1296 var lastComment = $comments.find('.comment-inline').last();
1297 if ($(lastComment).hasClass('comment-outdated')) {
1298 $replyWrapper.show();
1299 }
1300
1301 var _form = $($replyForm[0]).find('form');
1213 1302 var autocompleteActions = ['as_note', 'as_todo'];
1214 1303 var comment_id=null;
1215 var commentForm = this.createCommentForm(
1216 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1217
1218 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1219 form: _form,
1220 parent: $td[0],
1221 lineno: lineno,
1222 f_path: f_path}
1223 );
1304 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1305 var commentForm = self.createCommentForm(
1306 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1307 self.edit, comment_id);
1224 1308
1225 1309 // set a CUSTOM submit handler for inline comments.
1226 1310 commentForm.setHandleFormSubmit(function(o) {
1227 1311 var text = commentForm.cm.getValue();
1228 1312 var commentType = commentForm.getCommentType();
1229 1313 var resolvesCommentId = commentForm.getResolvesId();
1230 1314 var isDraft = commentForm.getDraftState();
1231 1315
1232 1316 if (text === "") {
1233 1317 return;
1234 1318 }
1235 1319
1236 if (lineno === undefined) {
1237 alert('missing line !');
1320 if (line_no === undefined) {
1321 alert('Error: unable to fetch line number for this inline comment !');
1238 1322 return;
1239 1323 }
1324
1240 1325 if (f_path === undefined) {
1241 alert('missing file path !');
1326 alert('Error: unable to fetch file path for this inline comment !');
1242 1327 return;
1243 1328 }
1244 1329
1245 1330 var excludeCancelBtn = false;
1246 1331 var submitEvent = true;
1247 1332 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1248 1333 commentForm.cm.setOption("readOnly", true);
1249 1334 var postData = {
1250 1335 'text': text,
1251 1336 'f_path': f_path,
1252 'line': lineno,
1337 'line': line_no,
1253 1338 'comment_type': commentType,
1254 1339 'draft': isDraft,
1255 1340 'csrf_token': CSRF_TOKEN
1256 1341 };
1257 1342 if (resolvesCommentId){
1258 1343 postData['resolves_comment_id'] = resolvesCommentId;
1259 1344 }
1260 1345
1346 // submitSuccess for inline commits
1261 1347 var submitSuccessCallback = function(json_data) {
1262 $form.remove();
1263 try {
1264 var html = json_data.rendered_text;
1265 var lineno = json_data.line_no;
1266 var target_id = json_data.target_id;
1348
1349 $replyForm.remove();
1350 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1351
1352 try {
1353
1354 // inject newly created comments, json_data is {<comment_id>: {}}
1355 self.attachInlineComment(json_data)
1267 1356
1268 $comments.find('.cb-comment-add-button').before(html);
1357 //mark visually which comment was resolved
1358 if (resolvesCommentId) {
1359 commentForm.markCommentResolved(resolvesCommentId);
1360 }
1269 1361
1270 //mark visually which comment was resolved
1271 if (resolvesCommentId) {
1272 commentForm.markCommentResolved(resolvesCommentId);
1362 // run global callback on submit
1363 commentForm.globalSubmitSuccessCallback({
1364 draft: isDraft,
1365 comment_id: comment_id
1366 });
1367
1368 } catch (e) {
1369 console.error(e);
1273 1370 }
1274 1371
1275 // run global callback on submit
1276 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1277
1278 } catch (e) {
1279 console.error(e);
1280 }
1281
1282 // re trigger the linkification of next/prev navigation
1283 linkifyComments($('.inline-comment-injected'));
1284 timeagoActivate();
1285 tooltipActivate();
1286
1287 1372 if (window.updateSticky !== undefined) {
1288 1373 // potentially our comments change the active window size, so we
1289 1374 // notify sticky elements
1290 1375 updateSticky()
1291 1376 }
1292 1377
1293 1378 if (window.refreshAllComments !== undefined && !isDraft) {
1294 1379 // if we have this handler, run it, and refresh all comments boxes
1295 1380 refreshAllComments()
1296 1381 }
1297 1382
1298 1383 commentForm.setActionButtonsDisabled(false);
1299 1384
1385 // re trigger the linkification of next/prev navigation
1386 linkifyComments($('.inline-comment-injected'));
1387 timeagoActivate();
1388 tooltipActivate();
1300 1389 };
1390
1301 1391 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1302 1392 var prefix = "Error while submitting comment.\n"
1303 1393 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1304 1394 ajaxErrorSwal(message);
1305 1395 commentForm.resetCommentFormState(text)
1306 1396 };
1397
1307 1398 commentForm.submitAjaxPOST(
1308 1399 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1309 1400 });
1310 1401 }
1311 1402
1312 $form.addClass('comment-inline-form-open');
1403 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1404 $replyForm.addClass('comment-inline-form-open');
1405 tooltipActivate();
1313 1406 };
1314 1407
1315 1408 this.createResolutionComment = function(commentId){
1316 1409 // hide the trigger text
1317 1410 $('#resolve-comment-{0}'.format(commentId)).hide();
1318 1411
1319 1412 var comment = $('#comment-'+commentId);
1320 1413 var commentData = comment.data();
1321 1414 if (commentData.commentInline) {
1322 this.createComment(comment, commentId)
1415 var f_path = commentData.fPath;
1416 var line_no = commentData.lineNo;
1417 //TODO check this if we need to give f_path/line_no
1418 this.createComment(comment, f_path, line_no, commentId)
1323 1419 } else {
1324 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1420 this.createGeneralComment('general', "$placeholder", commentId)
1325 1421 }
1326 1422
1327 1423 return false;
1328 1424 };
1329 1425
1330 1426 this.submitResolution = function(commentId){
1331 1427 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1332 1428 var commentForm = form.get(0).CommentForm;
1333 1429
1334 1430 var cm = commentForm.getCmInstance();
1335 1431 var renderer = templateContext.visual.default_renderer;
1336 1432 if (renderer == 'rst'){
1337 1433 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1338 1434 } else if (renderer == 'markdown') {
1339 1435 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1340 1436 } else {
1341 1437 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1342 1438 }
1343 1439
1344 1440 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1345 1441 form.submit();
1346 1442 return false;
1347 1443 };
1348 1444
1349 1445 };
1446
1447 window.commentHelp = function(renderer) {
1448 var funcData = {'renderer': renderer}
1449 return renderTemplate('commentHelpHovercard', funcData)
1450 } No newline at end of file
@@ -1,109 +1,119 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * QUICK REPO MENU, used on repositories to show shortcuts to files, history
21 21 * etc.
22 22 */
23 23
24 24 var quick_repo_menu = function() {
25 25 var hide_quick_repo_menus = function() {
26 26 $('.menu_items_container').hide();
27 27 $('.active_quick_repo').removeClass('active_quick_repo');
28 28 };
29 29 $('.quick_repo_menu').hover(function() {
30 30 hide_quick_repo_menus();
31 31 if (!$(this).hasClass('active_quick_repo')) {
32 32 $('.menu_items_container', this).removeClass("hidden").show();
33 33 $(this).addClass('active_quick_repo');
34 34 }
35 35 }, function() {
36 36 hide_quick_repo_menus();
37 37 });
38 38 };
39 39
40 40
41 41 window.toggleElement = function (elem, target) {
42 42 var $elem = $(elem);
43 43 var $target = $(target);
44 44
45 if ($target.is(':visible') || $target.length === 0) {
45 if (target !== undefined) {
46 var show = $target.is(':visible') || $target.length === 0;
47 } else {
48 var show = $elem.hasClass('toggle-off')
49 }
50
51 if (show) {
46 52 $target.hide();
47 53 $elem.html($elem.data('toggleOn'))
54 $elem.addClass('toggle-on')
55 $elem.removeClass('toggle-off')
48 56 } else {
49 57 $target.show();
50 58 $elem.html($elem.data('toggleOff'))
59 $elem.addClass('toggle-off')
60 $elem.removeClass('toggle-on')
51 61 }
52 62
53 63 return false
54 64 }
55 65
56 66 var marginExpVal = '300' // needs a sync with `.right-sidebar.right-sidebar-expanded` value
57 67 var marginColVal = '40' // needs a sync with `.right-sidebar.right-sidebar-collapsed` value
58 68
59 69 var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
60 70 var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
61 71
62 72 var updateStickyHeader = function () {
63 73 if (window.updateSticky !== undefined) {
64 74 // potentially our comments change the active window size, so we
65 75 // notify sticky elements
66 76 updateSticky()
67 77 }
68 78 }
69 79
70 80 var expandSidebar = function () {
71 81 var $sideBar = $('.right-sidebar');
72 82 $('.outerwrapper').css(marginExpanded);
73 83 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
74 84 $('.right-sidebar-collapsed-state').hide();
75 85 $('.right-sidebar-expanded-state').show();
76 86 $('.branding').addClass('display-none');
77 87 $sideBar.addClass('right-sidebar-expanded')
78 88 $sideBar.removeClass('right-sidebar-collapsed')
79 89 }
80 90
81 91 var collapseSidebar = function () {
82 92 var $sideBar = $('.right-sidebar');
83 93 $('.outerwrapper').css(marginCollapsed);
84 94 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
85 95 $('.right-sidebar-collapsed-state').show();
86 96 $('.right-sidebar-expanded-state').hide();
87 97 $('.branding').removeClass('display-none');
88 98 $sideBar.removeClass('right-sidebar-expanded')
89 99 $sideBar.addClass('right-sidebar-collapsed')
90 100 }
91 101
92 102 window.toggleSidebar = function () {
93 103 var $sideBar = $('.right-sidebar');
94 104
95 105 if ($sideBar.hasClass('right-sidebar-expanded')) {
96 106 // expanded -> collapsed transition
97 107 collapseSidebar();
98 108 var sidebarState = 'collapsed';
99 109
100 110 } else {
101 111 // collapsed -> expanded
102 112 expandSidebar();
103 113 var sidebarState = 'expanded';
104 114 }
105 115
106 116 // update our other sticky header in same context
107 117 updateStickyHeader();
108 118 storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
109 119 }
@@ -1,9 +1,7 b''
1 1 /__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
2 2 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
3 /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
4 /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
5 3 /notifications - shows new event notifications
6 4 /connection_controller/subscribe - subscribes user to new channels
7 5 /connection_controller/presence - receives presence change messages
8 6 /connection_controller/channel_update - receives channel states
9 7 /favicon/update - notify state change for favicon
@@ -1,4 +1,4 b''
1 1 ## this is a dummy html file for partial rendering on server and sending
2 2 ## generated output via ajax after comment submit
3 3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=c.co.is_inline)}
4 ${comment.comment_block(c.co, inline=c.co.is_inline, is_new=c.is_new)}
@@ -1,542 +1,560 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%!
9 9 from rhodecode.lib import html_filters
10 10 %>
11 11
12 12
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
14 14
15 15 <%
16 16 from rhodecode.model.comment import CommentsModel
17 17 comment_model = CommentsModel()
18 18
19 19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 20 latest_ver = len(getattr(c, 'versions', []))
21 21 visible_for_user = True
22 22 if comment.draft:
23 23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
24 24 %>
25 25
26 26 % if inline:
27 27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
28 28 % else:
29 29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
30 30 % endif
31 31
32 32 % if visible_for_user:
33 33 <div class="comment
34 34 ${'comment-inline' if inline else 'comment-general'}
35 35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
36 36 id="comment-${comment.comment_id}"
37 37 line="${comment.line_no}"
38 38 data-comment-id="${comment.comment_id}"
39 39 data-comment-type="${comment.comment_type}"
40 40 data-comment-draft=${h.json.dumps(comment.draft)}
41 41 data-comment-renderer="${comment.renderer}"
42 42 data-comment-text="${comment.text | html_filters.base64,n}"
43 data-comment-f-path="${comment.f_path}"
43 44 data-comment-line-no="${comment.line_no}"
44 45 data-comment-inline=${h.json.dumps(inline)}
45 46 style="${'display: none;' if outdated_at_ver else ''}">
46 47
47 48 <div class="meta">
48 49 <div class="comment-type-label">
49 50 % if comment.draft:
50 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">DRAFT</div>
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 DRAFT
53 </div>
51 54 % endif
55
56 % if is_new:
57 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
58 NEW
59 </div>
60 % endif
61
52 62 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
53 63
54 64 ## TODO COMMENT
55 65 % if comment.comment_type == 'todo':
56 66 % if comment.resolved:
57 67 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
58 68 <i class="icon-flag-filled"></i>
59 69 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
60 70 </div>
61 71 % else:
62 72 <div class="resolved tooltip" style="display: none">
63 73 <span>${comment.comment_type}</span>
64 74 </div>
65 75 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
66 76 <i class="icon-flag-filled"></i>
67 77 ${comment.comment_type}
68 78 </div>
69 79 % endif
70 80 ## NOTE COMMENT
71 81 % else:
72 82 ## RESOLVED NOTE
73 83 % if comment.resolved_comment:
74 84 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
75 85 fix
76 86 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
77 87 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
78 88 </a>
79 89 </div>
80 90 ## STATUS CHANGE NOTE
81 91 % elif not comment.is_inline and comment.status_change:
82 92 <%
83 93 if comment.pull_request:
84 94 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
85 95 else:
86 96 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
87 97 %>
88 98
89 99 <i class="icon-circle review-status-${comment.review_status}"></i>
90 100 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
91 101 ${comment.review_status_lbl}
92 102 </div>
93 103 % else:
94 104 <div>
95 105 <i class="icon-comment"></i>
96 106 ${(comment.comment_type or 'note')}
97 107 </div>
98 108 % endif
99 109 % endif
100 110
101 111 </div>
102 112 </div>
103 113 ## NOTE 0 and .. => because we disable it for now until UI ready
104 114 % if 0 and comment.status_change:
105 115 <div class="pull-left">
106 116 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
107 117 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
108 118 ${'!{}'.format(comment.pull_request.pull_request_id)}
109 119 </a>
110 120 </span>
111 121 </div>
112 122 % endif
113 123 ## Since only author can see drafts, we don't show it
114 124 % if not comment.draft:
115 125 <div class="author ${'author-inline' if inline else 'author-general'}">
116 126 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
117 127 </div>
118 128 % endif
119 129
120 130 <div class="date">
121 131 ${h.age_component(comment.modified_at, time_is_local=True)}
122 132 </div>
123 133
124 134 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
125 135 <span class="tag authortag tooltip" title="${_('Pull request author')}">
126 136 ${_('author')}
127 137 </span>
128 138 % endif
129 139
130 140 <%
131 141 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
132 142 %>
133 143
134 144 % if comment.history:
135 145 <div class="date">
136 146
137 147 <input id="${comment_version_selector}" name="${comment_version_selector}"
138 148 type="hidden"
139 149 data-last-version="${comment.history[-1].version}">
140 150
141 151 <script type="text/javascript">
142 152
143 153 var preLoadVersionData = [
144 154 % for comment_history in comment.history:
145 155 {
146 156 id: ${comment_history.comment_history_id},
147 157 text: 'v${comment_history.version}',
148 158 action: function () {
149 159 Rhodecode.comments.showVersion(
150 160 "${comment.comment_id}",
151 161 "${comment_history.comment_history_id}"
152 162 )
153 163 },
154 164 comment_version: "${comment_history.version}",
155 165 comment_author_username: "${comment_history.author.username}",
156 166 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
157 167 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
158 168 },
159 169 % endfor
160 170 ]
161 171 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
162 172
163 173 </script>
164 174
165 175 </div>
166 176 % else:
167 177 <div class="date" style="display: none">
168 178 <input id="${comment_version_selector}" name="${comment_version_selector}"
169 179 type="hidden"
170 180 data-last-version="0">
171 181 </div>
172 182 %endif
173 183
174 184 <div class="comment-links-block">
175 185
176 186 % if inline:
177 187 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
178 188 % if outdated_at_ver:
179 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
189 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
180 190 <code class="action-divider">|</code>
181 191 % elif comment_ver:
182 192 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
183 193 <code class="action-divider">|</code>
184 194 % endif
185 195 </a>
186 196 % else:
187 197 % if comment_ver:
188 198
189 199 % if comment.outdated:
190 200 <a class="pr-version"
191 201 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
192 202 >
193 203 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
194 204 </a>
195 205 <code class="action-divider">|</code>
196 206 % else:
197 207 <a class="tooltip pr-version"
198 208 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
199 209 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
200 210 >
201 211 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
202 212 </a>
203 213 <code class="action-divider">|</code>
204 214 % endif
205 215
206 216 % endif
207 217 % endif
208 218
209 219 <details class="details-reset details-inline-block">
210 220 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
211 221 <details-menu class="details-dropdown">
212 222
213 223 <div class="dropdown-item">
214 224 ${_('Comment')} #${comment.comment_id}
215 225 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
216 226 </div>
217 227
218 228 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
219 229 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
220 230 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
221 231 ## permissions to delete
222 232 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
223 233 <div class="dropdown-divider"></div>
224 234 <div class="dropdown-item">
225 <a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
235 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
226 236 </div>
227 237 <div class="dropdown-item">
228 238 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
229 239 </div>
230 % if comment.draft:
240 ## Only available in EE edition
241 % if comment.draft and c.rhodecode_edition_id == 'EE':
231 242 <div class="dropdown-item">
232 243 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
233 244 </div>
234 245 % endif
235 246 %else:
236 247 <div class="dropdown-divider"></div>
237 248 <div class="dropdown-item">
238 249 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
239 250 </div>
240 251 <div class="dropdown-item">
241 252 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
242 253 </div>
243 254 %endif
244 255 %else:
245 256 <div class="dropdown-divider"></div>
246 257 <div class="dropdown-item">
247 258 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
248 259 </div>
249 260 <div class="dropdown-item">
250 261 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
251 262 </div>
252 263 %endif
253 264 </details-menu>
254 265 </details>
255 266
256 267 <code class="action-divider">|</code>
257 268 % if outdated_at_ver:
258 269 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
259 270 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
260 271 % else:
261 272 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
262 273 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
263 274 % endif
264 275
265 276 </div>
266 277 </div>
267 278 <div class="text">
268 279 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
269 280 </div>
270 281
271 282 </div>
272 283 % endif
273 284 </%def>
274 285
275 286 ## generate main comments
276 287 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
277 288 <%
278 289 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
279 290 %>
280 291
281 292 <div class="general-comments" id="comments">
282 293 %for comment in comments:
283 294 <div id="comment-tr-${comment.comment_id}">
284 295 ## only render comments that are not from pull request, or from
285 296 ## pull request and a status change
286 297 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
287 298 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
288 299 %endif
289 300 </div>
290 301 %endfor
291 302 ## to anchor ajax comments
292 303 <div id="injected_page_comments"></div>
293 304 </div>
294 305 </%def>
295 306
296 307
297 308 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
298 309
299 310 <div class="comments">
300 311 <%
301 312 if is_pull_request:
302 313 placeholder = _('Leave a comment on this Pull Request.')
303 314 elif is_compare:
304 315 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
305 316 else:
306 317 placeholder = _('Leave a comment on this Commit.')
307 318 %>
308 319
309 320 % if c.rhodecode_user.username != h.DEFAULT_USER:
310 321 <div class="js-template" id="cb-comment-general-form-template">
311 322 ## template generated for injection
312 323 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
313 324 </div>
314 325
315 326 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
316 327 ## inject form here
317 328 </div>
318 329 <script type="text/javascript">
319 330 var lineNo = 'general';
320 331 var resolvesCommentId = null;
321 332 var generalCommentForm = Rhodecode.comments.createGeneralComment(
322 333 lineNo, "${placeholder}", resolvesCommentId);
323 334
324 335 // set custom success callback on rangeCommit
325 336 % if is_compare:
326 337 generalCommentForm.setHandleFormSubmit(function(o) {
327 338 var self = generalCommentForm;
328 339
329 340 var text = self.cm.getValue();
330 341 var status = self.getCommentStatus();
331 342 var commentType = self.getCommentType();
332 343 var isDraft = self.getDraftState();
333 344
334 345 if (text === "" && !status) {
335 346 return;
336 347 }
337 348
338 349 // we can pick which commits we want to make the comment by
339 350 // selecting them via click on preview pane, this will alter the hidden inputs
340 351 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
341 352
342 353 var commitIds = [];
343 354 $('#changeset_compare_view_content .compare_select').each(function(el) {
344 355 var commitId = this.id.replace('row-', '');
345 356 if ($(this).hasClass('hl') || !cherryPicked) {
346 357 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
347 358 commitIds.push(commitId);
348 359 } else {
349 360 $("input[data-commit-id='{0}']".format(commitId)).val('')
350 361 }
351 362 });
352 363
353 364 self.setActionButtonsDisabled(true);
354 365 self.cm.setOption("readOnly", true);
355 366 var postData = {
356 367 'text': text,
357 368 'changeset_status': status,
358 369 'comment_type': commentType,
359 370 'draft': isDraft,
360 371 'commit_ids': commitIds,
361 372 'csrf_token': CSRF_TOKEN
362 373 };
363 374
364 375 var submitSuccessCallback = function(o) {
365 376 location.reload(true);
366 377 };
367 378 var submitFailCallback = function(){
368 379 self.resetCommentFormState(text)
369 380 };
370 381 self.submitAjaxPOST(
371 382 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
372 383 });
373 384 % endif
374 385
375 386 </script>
376 387 % else:
377 388 ## form state when not logged in
378 389 <div class="comment-form ac">
379 390
380 391 <div class="comment-area">
381 392 <div class="comment-area-header">
382 393 <ul class="nav-links clearfix">
383 394 <li class="active">
384 395 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
385 396 </li>
386 397 <li class="">
387 398 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
388 399 </li>
389 400 </ul>
390 401 </div>
391 402
392 403 <div class="comment-area-write" style="display: block;">
393 404 <div id="edit-container">
394 <div style="padding: 40px 0">
405 <div style="padding: 20px 0px 0px 0;">
395 406 ${_('You need to be logged in to leave comments.')}
396 407 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
397 408 </div>
398 409 </div>
399 410 <div id="preview-container" class="clearfix" style="display: none;">
400 411 <div id="preview-box" class="preview-box"></div>
401 412 </div>
402 413 </div>
403 414
404 415 <div class="comment-area-footer">
405 416 <div class="toolbar">
406 417 <div class="toolbar-text">
407 418 </div>
408 419 </div>
409 420 </div>
410 421 </div>
411 422
412 423 <div class="comment-footer">
413 424 </div>
414 425
415 426 </div>
416 427 % endif
417 428
418 429 <script type="text/javascript">
419 430 bindToggleButtons();
420 431 </script>
421 432 </div>
422 433 </%def>
423 434
424 435
425 436 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
426 437
427 438 ## comment injected based on assumption that user is logged in
428 439 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
429 440
430 441 <div class="comment-area">
431 442 <div class="comment-area-header">
432 443 <div class="pull-left">
433 444 <ul class="nav-links clearfix">
434 445 <li class="active">
435 446 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
436 447 </li>
437 448 <li class="">
438 449 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
439 450 </li>
440 451 </ul>
441 452 </div>
442 453 <div class="pull-right">
443 454 <span class="comment-area-text">${_('Mark as')}:</span>
444 455 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
445 456 % for val in c.visual.comment_types:
446 457 <option value="${val}">${val.upper()}</option>
447 458 % endfor
448 459 </select>
449 460 </div>
450 461 </div>
451 462
452 463 <div class="comment-area-write" style="display: block;">
453 <div id="edit-container_${lineno_id}">
464 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
454 465 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
455 466 </div>
456 467 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
457 468 <div id="preview-box_${lineno_id}" class="preview-box"></div>
458 469 </div>
459 470 </div>
460 471
461 472 <div class="comment-area-footer comment-attachment-uploader">
462 473 <div class="toolbar">
463 474
464 475 <div class="comment-attachment-text">
465 476 <div class="dropzone-text">
466 477 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
467 478 </div>
468 479 <div class="dropzone-upload" style="display:none">
469 480 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
470 481 </div>
471 482 </div>
472 483
473 484 ## comments dropzone template, empty on purpose
474 485 <div style="display: none" class="comment-attachment-uploader-template">
475 486 <div class="dz-file-preview" style="margin: 0">
476 487 <div class="dz-error-message"></div>
477 488 </div>
478 489 </div>
479 490
480 491 </div>
481 492 </div>
482 493 </div>
483 494
484 495 <div class="comment-footer">
485 496
486 497 ## inject extra inputs into the form
487 498 % if form_extras and isinstance(form_extras, (list, tuple)):
488 499 <div id="comment_form_extras">
489 500 % for form_ex_el in form_extras:
490 501 ${form_ex_el|n}
491 502 % endfor
492 503 </div>
493 504 % endif
494 505
495 506 <div class="action-buttons">
496 507 % if form_type != 'inline':
497 508 <div class="action-buttons-extra"></div>
498 509 % endif
499 510
500 511 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
501 512
502 513 % if form_type == 'inline':
503 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
514 % if c.rhodecode_edition_id == 'EE':
515 ## Disable the button for CE, the "real" validation is in the backend code anyway
516 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
517 % else:
518 <input class="tooltip btn btn-warning comment-button-input submit-draft-action disabled" type="submit" disabled="disabled" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
519 % endif
520 % endif
521
522 % if review_statuses:
523 <div class="comment-status-box">
524 <select id="change_status_${lineno_id}" name="changeset_status">
525 <option></option> ## Placeholder
526 % for status, lbl in review_statuses:
527 <option value="${status}" data-status="${status}">${lbl}</option>
528 %if is_pull_request and change_status and status in ('approved', 'rejected'):
529 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
530 %endif
531 % endfor
532 </select>
533 </div>
504 534 % endif
505 535
506 536 ## inline for has a file, and line-number together with cancel hide button.
507 537 % if form_type == 'inline':
508 538 <input type="hidden" name="f_path" value="{0}">
509 539 <input type="hidden" name="line" value="${lineno_id}">
510 <button type="button" class="tooltip cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);" title="Hide comment box">
540 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
511 541 <i class="icon-cancel-circled2"></i>
512 542 </button>
513 543 % endif
514 544 </div>
515 545
516 % if review_statuses:
517 <div class="status_box">
518 <select id="change_status_${lineno_id}" name="changeset_status">
519 <option></option> ## Placeholder
520 % for status, lbl in review_statuses:
521 <option value="${status}" data-status="${status}">${lbl}</option>
522 %if is_pull_request and change_status and status in ('approved', 'rejected'):
523 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
524 %endif
525 % endfor
526 </select>
527 </div>
528 % endif
529
530 546 <div class="toolbar-text">
531 547 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
532 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
533 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
534 ${_('and')}
535 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
536 ${_('actions supported.')}
548 <p>${_('Styling with {} is supported.').format(renderer_url)|n}
549
550 <i class="icon-info-circled tooltip-hovercard"
551 data-hovercard-alt="ALT"
552 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
553 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
554 </p>
537 555 </div>
538 556 </div>
539 557
540 558 </form>
541 559
542 560 </%def> No newline at end of file
@@ -1,1406 +1,1415 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 3
3 4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 6 %></%def>
6 7
7 8 <%def name="action_class(action)">
8 9 <%
9 10 return {
10 11 '-': 'cb-deletion',
11 12 '+': 'cb-addition',
12 13 ' ': 'cb-context',
13 14 }.get(action, 'cb-empty')
14 15 %>
15 16 </%def>
16 17
17 18 <%def name="op_class(op_id)">
18 19 <%
19 20 return {
20 21 DEL_FILENODE: 'deletion', # file deleted
21 22 BIN_FILENODE: 'warning' # binary diff hidden
22 23 }.get(op_id, 'addition')
23 24 %>
24 25 </%def>
25 26
26 27
27 28
28 29 <%def name="render_diffset(diffset, commit=None,
29 30
30 31 # collapse all file diff entries when there are more than this amount of files in the diff
31 32 collapse_when_files_over=20,
32 33
33 34 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 35 lines_changed_limit=500,
35 36
36 37 # add a ruler at to the output
37 38 ruler_at_chars=0,
38 39
39 40 # show inline comments
40 41 use_comments=False,
41 42
42 43 # disable new comments
43 44 disable_new_comments=False,
44 45
45 46 # special file-comments that were deleted in previous versions
46 47 # it's used for showing outdated comments for deleted files in a PR
47 48 deleted_files_comments=None,
48 49
49 50 # for cache purpose
50 51 inline_comments=None,
51 52
52 53 # additional menu for PRs
53 54 pull_request_menu=None,
54 55
55 56 # show/hide todo next to comments
56 57 show_todos=True,
57 58
58 59 )">
59 60
60 61 <%
61 62 diffset_container_id = h.md5(diffset.target_ref)
62 63 collapse_all = len(diffset.files) > collapse_when_files_over
63 64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
66 67 %>
67 68
68 69 %if use_comments:
69 70
70 71 ## Template for injecting comments
71 72 <div id="cb-comments-inline-container-template" class="js-template">
72 73 ${inline_comments_container([])}
73 74 </div>
74 75
75 76 <div class="js-template" id="cb-comment-inline-form-template">
76 77 <div class="comment-inline-form ac">
77
78 %if c.rhodecode_user.username != h.DEFAULT_USER:
78 %if not c.rhodecode_user.is_default:
79 79 ## render template for inline comments
80 80 ${commentblock.comment_form(form_type='inline')}
81 %else:
82 ${h.form('', class_='inline-form comment-form-login', method='get')}
83 <div class="pull-left">
84 <div class="comment-help pull-right">
85 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
86 </div>
87 </div>
88 <div class="comment-button pull-right">
89 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
90 ${_('Cancel')}
91 </button>
92 </div>
93 <div class="clearfix"></div>
94 ${h.end_form()}
95 81 %endif
96 82 </div>
97 83 </div>
98 84
99 85 %endif
100 86
101 87 %if c.user_session_attrs["diffmode"] == 'sideside':
102 88 <style>
103 89 .wrapper {
104 90 max-width: 1600px !important;
105 91 }
106 92 </style>
107 93 %endif
108 94
109 95 %if ruler_at_chars:
110 96 <style>
111 97 .diff table.cb .cb-content:after {
112 98 content: "";
113 99 border-left: 1px solid blue;
114 100 position: absolute;
115 101 top: 0;
116 102 height: 18px;
117 103 opacity: .2;
118 104 z-index: 10;
119 105 //## +5 to account for diff action (+/-)
120 106 left: ${ruler_at_chars + 5}ch;
121 107 </style>
122 108 %endif
123 109
124 110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
125 111
126 112 <div style="height: 20px; line-height: 20px">
127 113 ## expand/collapse action
128 114 <div class="pull-left">
129 115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
130 116 % if collapse_all:
131 117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
132 118 % else:
133 119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
134 120 % endif
135 121 </a>
136 122
137 123 </div>
138 124
139 125 ## todos
140 126 % if show_todos and getattr(c, 'at_version', None):
141 127 <div class="pull-right">
142 128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
143 129 ${_('not available in this view')}
144 130 </div>
145 131 % elif show_todos:
146 132 <div class="pull-right">
147 133 <div class="comments-number" style="padding-left: 10px">
148 134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
149 135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
150 136 % if c.unresolved_comments:
151 137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
152 138 ${_('{} unresolved').format(len(c.unresolved_comments))}
153 139 </a>
154 140 % else:
155 141 ${_('0 unresolved')}
156 142 % endif
157 143
158 144 ${_('{} Resolved').format(len(c.resolved_comments))}
159 145 % endif
160 146 </div>
161 147 </div>
162 148 % endif
163 149
164 150 ## ## comments
165 151 ## <div class="pull-right">
166 152 ## <div class="comments-number" style="padding-left: 10px">
167 153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
168 154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
169 155 ## % if c.comments:
170 156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
171 157 ## % else:
172 158 ## ${_('0 General')}
173 159 ## % endif
174 160 ##
175 161 ## % if c.inline_cnt:
176 162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
177 163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
178 164 ## </a>
179 165 ## % else:
180 166 ## ${_('0 Inline')}
181 167 ## % endif
182 168 ## % endif
183 169 ##
184 170 ## % if pull_request_menu:
185 171 ## <%
186 172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
187 173 ## %>
188 174 ##
189 175 ## % if outdated_comm_count_ver:
190 176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
191 177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
192 178 ## </a>
193 179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
194 180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
195 181 ## % else:
196 182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
197 183 ## % endif
198 184 ##
199 185 ## % endif
200 186 ##
201 187 ## </div>
202 188 ## </div>
203 189
204 190 </div>
205 191
206 192 % if diffset.limited_diff:
207 193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
208 194 <h2 class="clearinner">
209 195 ${_('The requested changes are too big and content was truncated.')}
210 196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
211 197 </h2>
212 198 </div>
213 199 % endif
214 200
215 201 <div id="todo-box">
216 202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
217 203 % for co in c.unresolved_comments:
218 204 <a class="permalink" href="#comment-${co.comment_id}"
219 205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
220 206 <i class="icon-flag-filled-red"></i>
221 207 ${co.comment_id}</a>${('' if loop.last else ',')}
222 208 % endfor
223 209 % endif
224 210 </div>
225 211 %if diffset.has_hidden_changes:
226 212 <p class="empty_data">${_('Some changes may be hidden')}</p>
227 213 %elif not diffset.files:
228 214 <p class="empty_data">${_('No files')}</p>
229 215 %endif
230 216
231 217 <div class="filediffs">
232 218
233 219 ## initial value could be marked as False later on
234 220 <% over_lines_changed_limit = False %>
235 221 %for i, filediff in enumerate(diffset.files):
236 222
237 223 %if filediff.source_file_path and filediff.target_file_path:
238 224 %if filediff.source_file_path != filediff.target_file_path:
239 225 ## file was renamed, or copied
240 226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
241 227 <%
242 228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
243 229 final_path = filediff.target_file_path
244 230 %>
245 231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
246 232 <%
247 233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
248 234 final_path = filediff.target_file_path
249 235 %>
250 236 %endif
251 237 %else:
252 238 ## file was modified
253 239 <%
254 240 final_file_name = filediff.source_file_path
255 241 final_path = final_file_name
256 242 %>
257 243 %endif
258 244 %else:
259 245 %if filediff.source_file_path:
260 246 ## file was deleted
261 247 <%
262 248 final_file_name = filediff.source_file_path
263 249 final_path = final_file_name
264 250 %>
265 251 %else:
266 252 ## file was added
267 253 <%
268 254 final_file_name = filediff.target_file_path
269 255 final_path = final_file_name
270 256 %>
271 257 %endif
272 258 %endif
273 259
274 260 <%
275 261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
276 262 over_lines_changed_limit = lines_changed > lines_changed_limit
277 263 %>
278 264 ## anchor with support of sticky header
279 265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
280 266
281 267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
282 268 <div
283 269 class="filediff"
284 270 data-f-path="${filediff.patch['filename']}"
285 271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
286 272 >
287 273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
288 274 <%
289 275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
290 276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
291 277 %>
292 278 <div class="filediff-collapse-indicator icon-"></div>
293 279
294 280 ## Comments/Options PILL
295 281 <span class="pill-group pull-right">
296 282 <span class="pill" op="comments">
297 283 <i class="icon-comment"></i> ${len(total_file_comments)}
298 284 </span>
299 285
300 286 <details class="details-reset details-inline-block">
301 287 <summary class="noselect">
302 288 <i class="pill icon-options cursor-pointer" op="options"></i>
303 289 </summary>
304 290 <details-menu class="details-dropdown">
305 291
306 292 <div class="dropdown-item">
307 293 <span>${final_path}</span>
308 294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
309 295 </div>
310 296
311 297 <div class="dropdown-divider"></div>
312 298
313 299 <div class="dropdown-item">
314 300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
315 301 <a href="${permalink}">¶ permalink</a>
316 302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
317 303 </div>
318 304
319 305
320 306 </details-menu>
321 307 </details>
322 308
323 309 </span>
324 310
325 311 ${diff_ops(final_file_name, filediff)}
326 312
327 313 </label>
328 314
329 315 ${diff_menu(filediff, use_comments=use_comments)}
330 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
331 317
332 318 ## new/deleted/empty content case
333 319 % if not filediff.hunks:
334 320 ## Comment container, on "fakes" hunk that contains all data to render comments
335 321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
336 322 % endif
337 323
338 324 %if filediff.limited_diff:
339 325 <tr class="cb-warning cb-collapser">
340 326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
341 327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
342 328 </td>
343 329 </tr>
344 330 %else:
345 331 %if over_lines_changed_limit:
346 332 <tr class="cb-warning cb-collapser">
347 333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
348 334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
349 335 <a href="#" class="cb-expand"
350 336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
351 337 </a>
352 338 <a href="#" class="cb-collapse"
353 339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
354 340 </a>
355 341 </td>
356 342 </tr>
357 343 %endif
358 344 %endif
359 345
360 346 % for hunk in filediff.hunks:
361 347 <tr class="cb-hunk">
362 348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
363 349 ## TODO: dan: add ajax loading of more context here
364 350 ## <a href="#">
365 351 <i class="icon-more"></i>
366 352 ## </a>
367 353 </td>
368 354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
369 355 @@
370 356 -${hunk.source_start},${hunk.source_length}
371 357 +${hunk.target_start},${hunk.target_length}
372 358 ${hunk.section_header}
373 359 </td>
374 360 </tr>
375 361
376 362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
377 363 % endfor
378 364
379 365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
380 366
381 367 ## outdated comments that do not fit into currently displayed lines
382 368 % for lineno, comments in unmatched_comments.items():
383 369
384 370 %if c.user_session_attrs["diffmode"] == 'unified':
385 371 % if loop.index == 0:
386 372 <tr class="cb-hunk">
387 373 <td colspan="3"></td>
388 374 <td>
389 375 <div>
390 376 ${_('Unmatched/outdated inline comments below')}
391 377 </div>
392 378 </td>
393 379 </tr>
394 380 % endif
395 381 <tr class="cb-line">
396 382 <td class="cb-data cb-context"></td>
397 383 <td class="cb-lineno cb-context"></td>
398 384 <td class="cb-lineno cb-context"></td>
399 385 <td class="cb-content cb-context">
400 386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
401 387 </td>
402 388 </tr>
403 389 %elif c.user_session_attrs["diffmode"] == 'sideside':
404 390 % if loop.index == 0:
405 391 <tr class="cb-comment-info">
406 392 <td colspan="2"></td>
407 393 <td class="cb-line">
408 394 <div>
409 395 ${_('Unmatched/outdated inline comments below')}
410 396 </div>
411 397 </td>
412 398 <td colspan="2"></td>
413 399 <td class="cb-line">
414 400 <div>
415 401 ${_('Unmatched/outdated comments below')}
416 402 </div>
417 403 </td>
418 404 </tr>
419 405 % endif
420 406 <tr class="cb-line">
421 407 <td class="cb-data cb-context"></td>
422 408 <td class="cb-lineno cb-context"></td>
423 409 <td class="cb-content cb-context">
424 410 % if lineno.startswith('o'):
425 411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
426 412 % endif
427 413 </td>
428 414
429 415 <td class="cb-data cb-context"></td>
430 416 <td class="cb-lineno cb-context"></td>
431 417 <td class="cb-content cb-context">
432 418 % if lineno.startswith('n'):
433 419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
434 420 % endif
435 421 </td>
436 422 </tr>
437 423 %endif
438 424
439 425 % endfor
440 426
441 427 </table>
442 428 </div>
443 429 %endfor
444 430
445 431 ## outdated comments that are made for a file that has been deleted
446 432 % for filename, comments_dict in (deleted_files_comments or {}).items():
447 433
448 434 <%
449 435 display_state = 'display: none'
450 436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
451 437 if open_comments_in_file:
452 438 display_state = ''
453 439 fid = str(id(filename))
454 440 %>
455 441 <div class="filediffs filediff-outdated" style="${display_state}">
456 442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
457 443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
458 444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
459 445 <div class="filediff-collapse-indicator icon-"></div>
460 446
461 447 <span class="pill">
462 448 ## file was deleted
463 449 ${filename}
464 450 </span>
465 451 <span class="pill-group pull-left" >
466 452 ## file op, doesn't need translation
467 453 <span class="pill" op="removed">unresolved comments</span>
468 454 </span>
469 455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}"></a>
470 456 <span class="pill-group pull-right">
471 457 <span class="pill" op="deleted">
472 458 % if comments_dict['stats'] >0:
473 459 -${comments_dict['stats']}
474 460 % else:
475 461 ${comments_dict['stats']}
476 462 % endif
477 463 </span>
478 464 </span>
479 465 </label>
480 466
481 467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
482 468 <tr>
483 469 % if c.user_session_attrs["diffmode"] == 'unified':
484 470 <td></td>
485 471 %endif
486 472
487 473 <td></td>
488 474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
489 475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
490 476 ${_('There are still outdated/unresolved comments attached to it.')}
491 477 </td>
492 478 </tr>
493 479 %if c.user_session_attrs["diffmode"] == 'unified':
494 480 <tr class="cb-line">
495 481 <td class="cb-data cb-context"></td>
496 482 <td class="cb-lineno cb-context"></td>
497 483 <td class="cb-lineno cb-context"></td>
498 484 <td class="cb-content cb-context">
499 485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
500 486 </td>
501 487 </tr>
502 488 %elif c.user_session_attrs["diffmode"] == 'sideside':
503 489 <tr class="cb-line">
504 490 <td class="cb-data cb-context"></td>
505 491 <td class="cb-lineno cb-context"></td>
506 492 <td class="cb-content cb-context"></td>
507 493
508 494 <td class="cb-data cb-context"></td>
509 495 <td class="cb-lineno cb-context"></td>
510 496 <td class="cb-content cb-context">
511 497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
512 498 </td>
513 499 </tr>
514 500 %endif
515 501 </table>
516 502 </div>
517 503 </div>
518 504 % endfor
519 505
520 506 </div>
521 507 </div>
522 508 </%def>
523 509
524 510 <%def name="diff_ops(file_name, filediff)">
525 511 <%
526 512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
527 513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
528 514 %>
529 515 <span class="pill">
530 516 <i class="icon-file-text"></i>
531 517 ${file_name}
532 518 </span>
533 519
534 520 <span class="pill-group pull-right">
535 521
536 522 ## ops pills
537 523 %if filediff.limited_diff:
538 524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
539 525 %endif
540 526
541 527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
542 528 <span class="pill" op="created">created</span>
543 529 %if filediff['target_mode'].startswith('120'):
544 530 <span class="pill" op="symlink">symlink</span>
545 531 %else:
546 532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
547 533 %endif
548 534 %endif
549 535
550 536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
551 537 <span class="pill" op="renamed">renamed</span>
552 538 %endif
553 539
554 540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
555 541 <span class="pill" op="copied">copied</span>
556 542 %endif
557 543
558 544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
559 545 <span class="pill" op="removed">removed</span>
560 546 %endif
561 547
562 548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
563 549 <span class="pill" op="mode">
564 550 ${nice_mode(filediff['source_mode'])}${nice_mode(filediff['target_mode'])}
565 551 </span>
566 552 %endif
567 553
568 554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
569 555 <span class="pill" op="binary">binary</span>
570 556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
571 557 <span class="pill" op="modified">modified</span>
572 558 %endif
573 559 %endif
574 560
575 561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
576 562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
577 563
578 564 </span>
579 565
580 566 </%def>
581 567
582 568 <%def name="nice_mode(filemode)">
583 569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
584 570 </%def>
585 571
586 572 <%def name="diff_menu(filediff, use_comments=False)">
587 573 <div class="filediff-menu">
588 574
589 575 %if filediff.diffset.source_ref:
590 576
591 577 ## FILE BEFORE CHANGES
592 578 %if filediff.operation in ['D', 'M']:
593 579 <a
594 580 class="tooltip"
595 581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
596 582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
597 583 >
598 584 ${_('Show file before')}
599 585 </a> |
600 586 %else:
601 587 <span
602 588 class="tooltip"
603 589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
604 590 >
605 591 ${_('Show file before')}
606 592 </span> |
607 593 %endif
608 594
609 595 ## FILE AFTER CHANGES
610 596 %if filediff.operation in ['A', 'M']:
611 597 <a
612 598 class="tooltip"
613 599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
614 600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
615 601 >
616 602 ${_('Show file after')}
617 603 </a>
618 604 %else:
619 605 <span
620 606 class="tooltip"
621 607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
622 608 >
623 609 ${_('Show file after')}
624 610 </span>
625 611 %endif
626 612
627 613 % if use_comments:
628 614 |
629 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
630 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
631 619 </a>
632 620 % endif
633 621
634 622 %endif
635 623
636 624 </div>
637 625 </%def>
638 626
639 627
640 <%def name="inline_comments_container(comments, active_pattern_entries=None)">
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
641 629
642 630 <div class="inline-comments">
643 631 %for comment in comments:
644 632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
645 633 %endfor
646 % if comments and comments[-1].outdated:
647 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
648 ${_('Add another comment')}
649 </span>
650 % else:
651 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
652 ${_('Add another comment')}
653 </span>
654 % endif
634
635 <%
636 extra_class = ''
637 extra_style = ''
638
639 if comments and comments[-1].outdated:
640 extra_class = ' comment-outdated'
641 extra_style = 'display: none;'
655 642
643 %>
644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
645 <div class="reply-thread-container${extra_class}">
646 <div class="reply-thread-gravatar">
647 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
648 </div>
649 <div class="reply-thread-reply-button">
650 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
651 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
652 </div>
653 <div class="reply-thread-last"></div>
654 </div>
655 </div>
656 656 </div>
657
657 658 </%def>
658 659
659 660 <%!
660 661
661 662 def get_inline_comments(comments, filename):
662 663 if hasattr(filename, 'unicode_path'):
663 664 filename = filename.unicode_path
664 665
665 666 if not isinstance(filename, (unicode, str)):
666 667 return None
667 668
668 669 if comments and filename in comments:
669 670 return comments[filename]
670 671
671 672 return None
672 673
673 674 def get_comments_for(diff_type, comments, filename, line_version, line_number):
674 675 if hasattr(filename, 'unicode_path'):
675 676 filename = filename.unicode_path
676 677
677 678 if not isinstance(filename, (unicode, str)):
678 679 return None
679 680
680 681 file_comments = get_inline_comments(comments, filename)
681 682 if file_comments is None:
682 683 return None
683 684
684 685 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
685 686 if line_key in file_comments:
686 687 data = file_comments.pop(line_key)
687 688 return data
688 689 %>
689 690
690 691 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
691 692
692 693 <% chunk_count = 1 %>
693 694 %for loop_obj, item in h.looper(hunk.sideside):
694 695 <%
695 696 line = item
696 697 i = loop_obj.index
697 698 prev_line = loop_obj.previous
698 699 old_line_anchor, new_line_anchor = None, None
699 700
700 701 if line.original.lineno:
701 702 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
702 703 if line.modified.lineno:
703 704 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
704 705
705 706 line_action = line.modified.action or line.original.action
706 707 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
707 708 %>
708 709
709 710 <tr class="cb-line">
710 711 <td class="cb-data ${action_class(line.original.action)}"
711 712 data-line-no="${line.original.lineno}"
712 713 >
713 714
714 715 <% line_old_comments, line_old_comments_no_drafts = None, None %>
715 716 %if line.original.get_comment_args:
716 717 <%
717 718 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
718 719 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
719 720 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
720 721 %>
721 722 %endif
722 723 %if line_old_comments_no_drafts:
723 724 % if has_outdated:
724 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
725 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
725 726 % else:
726 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
727 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
727 728 % endif
728 729 %endif
729 730 </td>
730 731 <td class="cb-lineno ${action_class(line.original.action)}"
731 732 data-line-no="${line.original.lineno}"
732 733 %if old_line_anchor:
733 734 id="${old_line_anchor}"
734 735 %endif
735 736 >
736 737 %if line.original.lineno:
737 738 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
738 739 %endif
739 740 </td>
741
742 <% line_no = 'o{}'.format(line.original.lineno) %>
740 743 <td class="cb-content ${action_class(line.original.action)}"
741 data-line-no="o${line.original.lineno}"
744 data-line-no="${line_no}"
742 745 >
743 746 %if use_comments and line.original.lineno:
744 ${render_add_comment_button()}
747 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
745 748 %endif
746 749 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
747 750
748 751 %if use_comments and line.original.lineno and line_old_comments:
749 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
752 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
750 753 %endif
751 754
752 755 </td>
753 756 <td class="cb-data ${action_class(line.modified.action)}"
754 757 data-line-no="${line.modified.lineno}"
755 758 >
756 759 <div>
757 760
758 761 <% line_new_comments, line_new_comments_no_drafts = None, None %>
759 762 %if line.modified.get_comment_args:
760 763 <%
761 764 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
762 765 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
763 766 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
764 767 %>
765 768 %endif
766 769
767 770 %if line_new_comments_no_drafts:
768 771 % if has_outdated:
769 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
772 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
770 773 % else:
771 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
774 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
772 775 % endif
773 776 %endif
774 777 </div>
775 778 </td>
776 779 <td class="cb-lineno ${action_class(line.modified.action)}"
777 780 data-line-no="${line.modified.lineno}"
778 781 %if new_line_anchor:
779 782 id="${new_line_anchor}"
780 783 %endif
781 784 >
782 785 %if line.modified.lineno:
783 786 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
784 787 %endif
785 788 </td>
789
790 <% line_no = 'n{}'.format(line.modified.lineno) %>
786 791 <td class="cb-content ${action_class(line.modified.action)}"
787 data-line-no="n${line.modified.lineno}"
792 data-line-no="${line_no}"
788 793 >
789 794 %if use_comments and line.modified.lineno:
790 ${render_add_comment_button()}
795 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
791 796 %endif
792 797 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
793 %if use_comments and line.modified.lineno and line_new_comments:
794 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
795 %endif
796 798 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
797 799 <div class="nav-chunk" style="visibility: hidden">
798 800 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
799 801 </div>
800 802 <% chunk_count +=1 %>
801 803 % endif
804 %if use_comments and line.modified.lineno and line_new_comments:
805 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
806 %endif
807
802 808 </td>
803 809 </tr>
804 810 %endfor
805 811 </%def>
806 812
807 813
808 814 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
809 815 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
810 816
811 817 <%
812 818 old_line_anchor, new_line_anchor = None, None
813 819 if old_line_no:
814 820 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
815 821 if new_line_no:
816 822 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
817 823 %>
818 824 <tr class="cb-line">
819 825 <td class="cb-data ${action_class(action)}">
820 826 <div>
821 827
822 828 <% comments, comments_no_drafts = None, None %>
823 829 %if comments_args:
824 830 <%
825 831 comments = get_comments_for('unified', inline_comments, *comments_args)
826 832 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
827 833 has_outdated = any([x.outdated for x in comments_no_drafts])
828 834 %>
829 835 %endif
830 836
831 837 % if comments_no_drafts:
832 838 % if has_outdated:
833 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
839 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
834 840 % else:
835 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
841 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
836 842 % endif
837 843 % endif
838 844 </div>
839 845 </td>
840 846 <td class="cb-lineno ${action_class(action)}"
841 847 data-line-no="${old_line_no}"
842 848 %if old_line_anchor:
843 849 id="${old_line_anchor}"
844 850 %endif
845 851 >
846 852 %if old_line_anchor:
847 853 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
848 854 %endif
849 855 </td>
850 856 <td class="cb-lineno ${action_class(action)}"
851 857 data-line-no="${new_line_no}"
852 858 %if new_line_anchor:
853 859 id="${new_line_anchor}"
854 860 %endif
855 861 >
856 862 %if new_line_anchor:
857 863 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
858 864 %endif
859 865 </td>
866 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
860 867 <td class="cb-content ${action_class(action)}"
861 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
868 data-line-no="${line_no}"
862 869 >
863 870 %if use_comments:
864 ${render_add_comment_button()}
871 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
865 872 %endif
866 873 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
867 874 %if use_comments and comments:
868 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
875 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
869 876 %endif
870 877 </td>
871 878 </tr>
872 879 %endfor
873 880 </%def>
874 881
875 882
876 883 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
877 884 % if diff_mode == 'unified':
878 885 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
879 886 % elif diff_mode == 'sideside':
880 887 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
881 888 % else:
882 889 <tr class="cb-line">
883 890 <td>unknown diff mode</td>
884 891 </tr>
885 892 % endif
886 893 </%def>file changes
887 894
888 895
889 <%def name="render_add_comment_button()">
890 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
896 <%def name="render_add_comment_button(line_no='', f_path='')">
897 % if not c.rhodecode_user.is_default:
898 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
891 899 <span><i class="icon-comment"></i></span>
892 900 </button>
901 % endif
893 902 </%def>
894 903
895 904 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
896 905 <% diffset_container_id = h.md5(diffset.target_ref) %>
897 906
898 907 <div id="diff-file-sticky" class="diffset-menu clearinner">
899 908 ## auto adjustable
900 909 <div class="sidebar__inner">
901 910 <div class="sidebar__bar">
902 911 <div class="pull-right">
903 912 <div class="btn-group">
904 913 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
905 914 <i class="icon-wide-mode"></i>
906 915 </a>
907 916 </div>
908 917 <div class="btn-group">
909 918
910 919 <a
911 920 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
912 921 title="${h.tooltip(_('View diff as side by side'))}"
913 922 href="${h.current_route_path(request, diffmode='sideside')}">
914 923 <span>${_('Side by Side')}</span>
915 924 </a>
916 925
917 926 <a
918 927 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
919 928 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
920 929 <span>${_('Unified')}</span>
921 930 </a>
922 931
923 932 % if range_diff_on is True:
924 933 <a
925 934 title="${_('Turn off: Show the diff as commit range')}"
926 935 class="btn btn-primary"
927 936 href="${h.current_route_path(request, **{"range-diff":"0"})}">
928 937 <span>${_('Range Diff')}</span>
929 938 </a>
930 939 % elif range_diff_on is False:
931 940 <a
932 941 title="${_('Show the diff as commit range')}"
933 942 class="btn"
934 943 href="${h.current_route_path(request, **{"range-diff":"1"})}">
935 944 <span>${_('Range Diff')}</span>
936 945 </a>
937 946 % endif
938 947 </div>
939 948 <div class="btn-group">
940 949
941 950 <div class="pull-left">
942 951 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
943 952 </div>
944 953
945 954 </div>
946 955 </div>
947 956 <div class="pull-left">
948 957 <div class="btn-group">
949 958 <div class="pull-left">
950 959 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
951 960 </div>
952 961
953 962 </div>
954 963 </div>
955 964 </div>
956 965 <div class="fpath-placeholder pull-left">
957 966 <i class="icon-file-text"></i>
958 967 <strong class="fpath-placeholder-text">
959 968 Context file:
960 969 </strong>
961 970 </div>
962 971 <div class="pull-right noselect">
963 972
964 973 %if commit:
965 974 <span>
966 975 <code>${h.show_id(commit)}</code>
967 976 </span>
968 977 %elif pull_request_menu and pull_request_menu.get('pull_request'):
969 978 <span>
970 979 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
971 980 </span>
972 981 %endif
973 982 % if commit or pull_request_menu:
974 983 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
975 984 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
976 985 <i class="icon-angle-up"></i>
977 986 </span>
978 987 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
979 988 <i class="icon-angle-down"></i>
980 989 </span>
981 990 % endif
982 991 </div>
983 992 <div class="sidebar_inner_shadow"></div>
984 993 </div>
985 994 </div>
986 995
987 996 % if diffset:
988 997 %if diffset.limited_diff:
989 998 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
990 999 %else:
991 1000 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
992 1001 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
993 1002
994 1003 %endif
995 1004 ## case on range-diff placeholder needs to be updated
996 1005 % if range_diff_on is True:
997 1006 <% file_placeholder = _('Disabled on range diff') %>
998 1007 % endif
999 1008
1000 1009 <script type="text/javascript">
1001 1010 var feedFilesOptions = function (query, initialData) {
1002 1011 var data = {results: []};
1003 1012 var isQuery = typeof query.term !== 'undefined';
1004 1013
1005 1014 var section = _gettext('Changed files');
1006 1015 var filteredData = [];
1007 1016
1008 1017 //filter results
1009 1018 $.each(initialData.results, function (idx, value) {
1010 1019
1011 1020 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1012 1021 filteredData.push({
1013 1022 'id': this.id,
1014 1023 'text': this.text,
1015 1024 "ops": this.ops,
1016 1025 })
1017 1026 }
1018 1027
1019 1028 });
1020 1029
1021 1030 data.results = filteredData;
1022 1031
1023 1032 query.callback(data);
1024 1033 };
1025 1034
1026 1035 var selectionFormatter = function(data, escapeMarkup) {
1027 1036 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1028 1037 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1029 1038 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1030 1039 '<span class="pill" op="added">{0}</span>' +
1031 1040 '<span class="pill" op="deleted">{1}</span>' +
1032 1041 '</div>'
1033 1042 ;
1034 1043 var added = data['ops']['added'];
1035 1044 if (added === 0) {
1036 1045 // don't show +0
1037 1046 added = 0;
1038 1047 } else {
1039 1048 added = '+' + added;
1040 1049 }
1041 1050
1042 1051 var deleted = -1*data['ops']['deleted'];
1043 1052
1044 1053 tmpl += pill.format(added, deleted);
1045 1054 return container.format(tmpl);
1046 1055 };
1047 1056 var formatFileResult = function(result, container, query, escapeMarkup) {
1048 1057 return selectionFormatter(result, escapeMarkup);
1049 1058 };
1050 1059
1051 1060 var formatSelection = function (data, container) {
1052 1061 return '${file_placeholder}'
1053 1062 };
1054 1063
1055 1064 if (window.preloadFileFilterData === undefined) {
1056 1065 window.preloadFileFilterData = {}
1057 1066 }
1058 1067
1059 1068 preloadFileFilterData["${diffset_container_id}"] = {
1060 1069 results: [
1061 1070 % for filediff in diffset.files:
1062 1071 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1063 1072 text:"${filediff.patch['filename']}",
1064 1073 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1065 1074 % endfor
1066 1075 ]
1067 1076 };
1068 1077
1069 1078 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1070 1079 var diffFileFilter = $(diffFileFilterId).select2({
1071 1080 'dropdownAutoWidth': true,
1072 1081 'width': 'auto',
1073 1082
1074 1083 containerCssClass: "drop-menu",
1075 1084 dropdownCssClass: "drop-menu-dropdown",
1076 1085 data: preloadFileFilterData["${diffset_container_id}"],
1077 1086 query: function(query) {
1078 1087 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1079 1088 },
1080 1089 initSelection: function(element, callback) {
1081 1090 callback({'init': true});
1082 1091 },
1083 1092 formatResult: formatFileResult,
1084 1093 formatSelection: formatSelection
1085 1094 });
1086 1095
1087 1096 % if range_diff_on is True:
1088 1097 diffFileFilter.select2("enable", false);
1089 1098 % endif
1090 1099
1091 1100 $(diffFileFilterId).on('select2-selecting', function (e) {
1092 1101 var idSelector = e.choice.id;
1093 1102
1094 1103 // expand the container if we quick-select the field
1095 1104 $('#'+idSelector).next().prop('checked', false);
1096 1105 // hide the mast as we later do preventDefault()
1097 1106 $("#select2-drop-mask").click();
1098 1107
1099 1108 window.location.hash = '#'+idSelector;
1100 1109 updateSticky();
1101 1110
1102 1111 e.preventDefault();
1103 1112 });
1104 1113
1105 1114 diffNavText = 'diff navigation:'
1106 1115
1107 1116 getCurrentChunk = function () {
1108 1117
1109 1118 var chunksAll = $('.nav-chunk').filter(function () {
1110 1119 return $(this).parents('.filediff').prev().get(0).checked !== true
1111 1120 })
1112 1121 var chunkSelected = $('.nav-chunk.selected');
1113 1122 var initial = false;
1114 1123
1115 1124 if (chunkSelected.length === 0) {
1116 1125 // no initial chunk selected, we pick first
1117 1126 chunkSelected = $(chunksAll.get(0));
1118 1127 var initial = true;
1119 1128 }
1120 1129
1121 1130 return {
1122 1131 'all': chunksAll,
1123 1132 'selected': chunkSelected,
1124 1133 'initial': initial,
1125 1134 }
1126 1135 }
1127 1136
1128 1137 animateDiffNavText = function () {
1129 1138 var $diffNav = $('#diff_nav')
1130 1139
1131 1140 var callback = function () {
1132 1141 $diffNav.animate({'opacity': 1.00}, 200)
1133 1142 };
1134 1143 $diffNav.animate({'opacity': 0.15}, 200, callback);
1135 1144 }
1136 1145
1137 1146 scrollToChunk = function (moveBy) {
1138 1147 var chunk = getCurrentChunk();
1139 1148 var all = chunk.all
1140 1149 var selected = chunk.selected
1141 1150
1142 1151 var curPos = all.index(selected);
1143 1152 var newPos = curPos;
1144 1153 if (!chunk.initial) {
1145 1154 var newPos = curPos + moveBy;
1146 1155 }
1147 1156
1148 1157 var curElem = all.get(newPos);
1149 1158
1150 1159 if (curElem === undefined) {
1151 1160 // end or back
1152 1161 $('#diff_nav').html('no next diff element:')
1153 1162 animateDiffNavText()
1154 1163 return
1155 1164 } else if (newPos < 0) {
1156 1165 $('#diff_nav').html('no previous diff element:')
1157 1166 animateDiffNavText()
1158 1167 return
1159 1168 } else {
1160 1169 $('#diff_nav').html(diffNavText)
1161 1170 }
1162 1171
1163 1172 curElem = $(curElem)
1164 1173 var offset = 100;
1165 1174 $(window).scrollTop(curElem.position().top - offset);
1166 1175
1167 1176 //clear selection
1168 1177 all.removeClass('selected')
1169 1178 curElem.addClass('selected')
1170 1179 }
1171 1180
1172 1181 scrollToPrevChunk = function () {
1173 1182 scrollToChunk(-1)
1174 1183 }
1175 1184 scrollToNextChunk = function () {
1176 1185 scrollToChunk(1)
1177 1186 }
1178 1187
1179 1188 </script>
1180 1189 % endif
1181 1190
1182 1191 <script type="text/javascript">
1183 1192 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1184 1193
1185 1194 $(document).ready(function () {
1186 1195
1187 1196 var contextPrefix = _gettext('Context file: ');
1188 1197 ## sticky sidebar
1189 1198 var sidebarElement = document.getElementById('diff-file-sticky');
1190 1199 sidebar = new StickySidebar(sidebarElement, {
1191 1200 topSpacing: 0,
1192 1201 bottomSpacing: 0,
1193 1202 innerWrapperSelector: '.sidebar__inner'
1194 1203 });
1195 1204 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1196 1205 // reset our file so it's not holding new value
1197 1206 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1198 1207 });
1199 1208
1200 1209 updateSticky = function () {
1201 1210 sidebar.updateSticky();
1202 1211 Waypoint.refreshAll();
1203 1212 };
1204 1213
1205 1214 var animateText = function (fPath, anchorId) {
1206 1215 fPath = Select2.util.escapeMarkup(fPath);
1207 1216 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1208 1217 };
1209 1218
1210 1219 ## dynamic file waypoints
1211 1220 var setFPathInfo = function(fPath, anchorId){
1212 1221 animateText(fPath, anchorId)
1213 1222 };
1214 1223
1215 1224 var codeBlock = $('.filediff');
1216 1225
1217 1226 // forward waypoint
1218 1227 codeBlock.waypoint(
1219 1228 function(direction) {
1220 1229 if (direction === "down"){
1221 1230 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1222 1231 }
1223 1232 }, {
1224 1233 offset: function () {
1225 1234 return 70;
1226 1235 },
1227 1236 context: '.fpath-placeholder'
1228 1237 }
1229 1238 );
1230 1239
1231 1240 // backward waypoint
1232 1241 codeBlock.waypoint(
1233 1242 function(direction) {
1234 1243 if (direction === "up"){
1235 1244 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1236 1245 }
1237 1246 }, {
1238 1247 offset: function () {
1239 1248 return -this.element.clientHeight + 90;
1240 1249 },
1241 1250 context: '.fpath-placeholder'
1242 1251 }
1243 1252 );
1244 1253
1245 1254 toggleWideDiff = function (el) {
1246 1255 updateSticky();
1247 1256 var wide = Rhodecode.comments.toggleWideMode(this);
1248 1257 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1249 1258 if (wide === true) {
1250 1259 $(el).addClass('btn-active');
1251 1260 } else {
1252 1261 $(el).removeClass('btn-active');
1253 1262 }
1254 1263 return null;
1255 1264 };
1256 1265
1257 1266 var preloadDiffMenuData = {
1258 1267 results: [
1259 1268
1260 1269 ## Whitespace change
1261 1270 % if request.GET.get('ignorews', '') == '1':
1262 1271 {
1263 1272 id: 2,
1264 1273 text: _gettext('Show whitespace changes'),
1265 1274 action: function () {},
1266 1275 url: "${h.current_route_path(request, ignorews=0)|n}"
1267 1276 },
1268 1277 % else:
1269 1278 {
1270 1279 id: 2,
1271 1280 text: _gettext('Hide whitespace changes'),
1272 1281 action: function () {},
1273 1282 url: "${h.current_route_path(request, ignorews=1)|n}"
1274 1283 },
1275 1284 % endif
1276 1285
1277 1286 ## FULL CONTEXT
1278 1287 % if request.GET.get('fullcontext', '') == '1':
1279 1288 {
1280 1289 id: 3,
1281 1290 text: _gettext('Hide full context diff'),
1282 1291 action: function () {},
1283 1292 url: "${h.current_route_path(request, fullcontext=0)|n}"
1284 1293 },
1285 1294 % else:
1286 1295 {
1287 1296 id: 3,
1288 1297 text: _gettext('Show full context diff'),
1289 1298 action: function () {},
1290 1299 url: "${h.current_route_path(request, fullcontext=1)|n}"
1291 1300 },
1292 1301 % endif
1293 1302
1294 1303 ]
1295 1304 };
1296 1305
1297 1306 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1298 1307 $(diffMenuId).select2({
1299 1308 minimumResultsForSearch: -1,
1300 1309 containerCssClass: "drop-menu-no-width",
1301 1310 dropdownCssClass: "drop-menu-dropdown",
1302 1311 dropdownAutoWidth: true,
1303 1312 data: preloadDiffMenuData,
1304 1313 placeholder: "${_('...')}",
1305 1314 });
1306 1315 $(diffMenuId).on('select2-selecting', function (e) {
1307 1316 e.choice.action();
1308 1317 if (e.choice.url !== null) {
1309 1318 window.location = e.choice.url
1310 1319 }
1311 1320 });
1312 1321 toggleExpand = function (el, diffsetEl) {
1313 1322 var el = $(el);
1314 1323 if (el.hasClass('collapsed')) {
1315 1324 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1316 1325 el.removeClass('collapsed');
1317 1326 el.html(
1318 1327 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1319 1328 _gettext('Collapse all files'));
1320 1329 }
1321 1330 else {
1322 1331 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1323 1332 el.addClass('collapsed');
1324 1333 el.html(
1325 1334 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1326 1335 _gettext('Expand all files'));
1327 1336 }
1328 1337 updateSticky()
1329 1338 };
1330 1339
1331 1340 toggleCommitExpand = function (el) {
1332 1341 var $el = $(el);
1333 1342 var commits = $el.data('toggleCommitsCnt');
1334 1343 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1335 1344 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1336 1345
1337 1346 if ($el.hasClass('collapsed')) {
1338 1347 $('.compare_select').show();
1339 1348 $('.compare_select_hidden').hide();
1340 1349
1341 1350 $el.removeClass('collapsed');
1342 1351 $el.html(
1343 1352 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1344 1353 collapseMsg);
1345 1354 }
1346 1355 else {
1347 1356 $('.compare_select').hide();
1348 1357 $('.compare_select_hidden').show();
1349 1358 $el.addClass('collapsed');
1350 1359 $el.html(
1351 1360 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1352 1361 expandMsg);
1353 1362 }
1354 1363 updateSticky();
1355 1364 };
1356 1365
1357 1366 // get stored diff mode and pre-enable it
1358 1367 if (templateContext.session_attrs.wide_diff_mode === "true") {
1359 1368 Rhodecode.comments.toggleWideMode(null);
1360 1369 $('.toggle-wide-diff').addClass('btn-active');
1361 1370 updateSticky();
1362 1371 }
1363 1372
1364 1373 // DIFF NAV //
1365 1374
1366 1375 // element to detect scroll direction of
1367 1376 var $window = $(window);
1368 1377
1369 1378 // initialize last scroll position
1370 1379 var lastScrollY = $window.scrollTop();
1371 1380
1372 1381 $window.on('resize scrollstop', {latency: 350}, function () {
1373 1382 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1374 1383
1375 1384 // get current scroll position
1376 1385 var currentScrollY = $window.scrollTop();
1377 1386
1378 1387 // determine current scroll direction
1379 1388 if (currentScrollY > lastScrollY) {
1380 1389 var y = 'down'
1381 1390 } else if (currentScrollY !== lastScrollY) {
1382 1391 var y = 'up';
1383 1392 }
1384 1393
1385 1394 var pos = -1; // by default we use last element in viewport
1386 1395 if (y === 'down') {
1387 1396 pos = -1;
1388 1397 } else if (y === 'up') {
1389 1398 pos = 0;
1390 1399 }
1391 1400
1392 1401 if (visibleChunks.length > 0) {
1393 1402 $('.nav-chunk').removeClass('selected');
1394 1403 $(visibleChunks.get(pos)).addClass('selected');
1395 1404 }
1396 1405
1397 1406 // update last scroll position to current position
1398 1407 lastScrollY = currentScrollY;
1399 1408
1400 1409 });
1401 1410 $('#diff_nav').html(diffNavText);
1402 1411
1403 1412 });
1404 1413 </script>
1405 1414
1406 1415 </%def>
@@ -1,489 +1,489 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 <%def name="metatags_help()">
7 7 <table>
8 8 <%
9 9 example_tags = [
10 10 ('state','[stable]'),
11 11 ('state','[stale]'),
12 12 ('state','[featured]'),
13 13 ('state','[dev]'),
14 14 ('state','[dead]'),
15 15 ('state','[deprecated]'),
16 16
17 17 ('label','[personal]'),
18 18 ('generic','[v2.0.0]'),
19 19
20 20 ('lang','[lang =&gt; JavaScript]'),
21 21 ('license','[license =&gt; LicenseName]'),
22 22
23 23 ('ref','[requires =&gt; RepoName]'),
24 24 ('ref','[recommends =&gt; GroupName]'),
25 25 ('ref','[conflicts =&gt; SomeName]'),
26 26 ('ref','[base =&gt; SomeName]'),
27 27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
28 28 ('see','[see =&gt; http://rhodecode.com]'),
29 29 ]
30 30 %>
31 31 % for tag_type, tag in example_tags:
32 32 <tr>
33 33 <td>${tag|n}</td>
34 34 <td>${h.style_metatag(tag_type, tag)|n}</td>
35 35 </tr>
36 36 % endfor
37 37 </table>
38 38 </%def>
39 39
40 40 <%def name="render_description(description, stylify_metatags)">
41 41 <%
42 42 tags = []
43 43 if stylify_metatags:
44 44 tags, description = h.extract_metatags(description)
45 45 %>
46 46 % for tag_type, tag in tags:
47 47 ${h.style_metatag(tag_type, tag)|n,trim}
48 48 % endfor
49 49 <code style="white-space: pre-wrap">${description}</code>
50 50 </%def>
51 51
52 52 ## REPOSITORY RENDERERS
53 53 <%def name="quick_menu(repo_name)">
54 54 <i class="icon-more"></i>
55 55 <div class="menu_items_container hidden">
56 56 <ul class="menu_items">
57 57 <li>
58 58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
59 59 <span>${_('Summary')}</span>
60 60 </a>
61 61 </li>
62 62 <li>
63 63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
64 64 <span>${_('Commits')}</span>
65 65 </a>
66 66 </li>
67 67 <li>
68 68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
69 69 <span>${_('Files')}</span>
70 70 </a>
71 71 </li>
72 72 <li>
73 73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
74 74 <span>${_('Fork')}</span>
75 75 </a>
76 76 </li>
77 77 </ul>
78 78 </div>
79 79 </%def>
80 80
81 81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
82 82 <%
83 83 def get_name(name,short_name=short_name):
84 84 if short_name:
85 85 return name.split('/')[-1]
86 86 else:
87 87 return name
88 88 %>
89 89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
90 90 ##NAME
91 91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
92 92
93 93 ##TYPE OF REPO
94 94 %if h.is_hg(rtype):
95 95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
96 96 %elif h.is_git(rtype):
97 97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
98 98 %elif h.is_svn(rtype):
99 99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
100 100 %endif
101 101
102 102 ##PRIVATE/PUBLIC
103 103 %if private is True and c.visual.show_private_icon:
104 104 <i class="icon-lock" title="${_('Private repository')}"></i>
105 105 %elif private is False and c.visual.show_public_icon:
106 106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
107 107 %else:
108 108 <span></span>
109 109 %endif
110 110 ${get_name(name)}
111 111 </a>
112 112 %if fork_of:
113 113 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
114 114 %endif
115 115 %if rstate == 'repo_state_pending':
116 116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
117 117 (${_('creating...')})
118 118 </span>
119 119 %endif
120 120
121 121 </div>
122 122 </%def>
123 123
124 124 <%def name="repo_desc(description, stylify_metatags)">
125 125 <%
126 126 tags, description = h.extract_metatags(description)
127 127 %>
128 128
129 129 <div class="truncate-wrap">
130 130 % if stylify_metatags:
131 131 % for tag_type, tag in tags:
132 132 ${h.style_metatag(tag_type, tag)|n}
133 133 % endfor
134 134 % endif
135 135 ${description}
136 136 </div>
137 137
138 138 </%def>
139 139
140 140 <%def name="last_change(last_change)">
141 141 ${h.age_component(last_change, time_is_local=True)}
142 142 </%def>
143 143
144 144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
145 145 <div>
146 146 %if rev >= 0:
147 147 <code><a class="tooltip-hovercard" data-hovercard-alt=${h.tooltip(last_msg)} data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
148 148 %else:
149 149 ${_('No commits yet')}
150 150 %endif
151 151 </div>
152 152 </%def>
153 153
154 154 <%def name="rss(name)">
155 155 %if c.rhodecode_user.username != h.DEFAULT_USER:
156 156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
157 157 %else:
158 158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
159 159 %endif
160 160 </%def>
161 161
162 162 <%def name="atom(name)">
163 163 %if c.rhodecode_user.username != h.DEFAULT_USER:
164 164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
165 165 %else:
166 166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
167 167 %endif
168 168 </%def>
169 169
170 170 <%def name="repo_actions(repo_name, super_user=True)">
171 171 <div>
172 172 <div class="grid_edit">
173 173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
174 174 Edit
175 175 </a>
176 176 </div>
177 177 <div class="grid_delete">
178 178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
179 179 <input class="btn btn-link btn-danger" id="remove_${repo_name}" name="remove_${repo_name}"
180 180 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${repo_name}')"
181 181 type="submit" value="Delete"
182 182 >
183 183 ${h.end_form()}
184 184 </div>
185 185 </div>
186 186 </%def>
187 187
188 188 <%def name="repo_state(repo_state)">
189 189 <div>
190 190 %if repo_state == 'repo_state_pending':
191 191 <div class="tag tag4">${_('Creating')}</div>
192 192 %elif repo_state == 'repo_state_created':
193 193 <div class="tag tag1">${_('Created')}</div>
194 194 %else:
195 195 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
196 196 %endif
197 197 </div>
198 198 </%def>
199 199
200 200
201 201 ## REPO GROUP RENDERERS
202 202 <%def name="quick_repo_group_menu(repo_group_name)">
203 203 <i class="icon-more"></i>
204 204 <div class="menu_items_container hidden">
205 205 <ul class="menu_items">
206 206 <li>
207 207 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
208 208 </li>
209 209
210 210 </ul>
211 211 </div>
212 212 </%def>
213 213
214 214 <%def name="repo_group_name(repo_group_name, children_groups=None)">
215 215 <div>
216 216 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
217 217 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
218 218 %if children_groups:
219 219 ${h.literal(' &raquo; '.join(children_groups))}
220 220 %else:
221 221 ${repo_group_name}
222 222 %endif
223 223 </a>
224 224 </div>
225 225 </%def>
226 226
227 227 <%def name="repo_group_desc(description, personal, stylify_metatags)">
228 228
229 229 <%
230 230 if stylify_metatags:
231 231 tags, description = h.extract_metatags(description)
232 232 %>
233 233
234 234 <div class="truncate-wrap">
235 235 % if personal:
236 236 <div class="metatag" tag="personal">${_('personal')}</div>
237 237 % endif
238 238
239 239 % if stylify_metatags:
240 240 % for tag_type, tag in tags:
241 241 ${h.style_metatag(tag_type, tag)|n}
242 242 % endfor
243 243 % endif
244 244 ${description}
245 245 </div>
246 246
247 247 </%def>
248 248
249 249 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
250 250 <div class="grid_edit">
251 251 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
252 252 </div>
253 253 <div class="grid_delete">
254 254 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
255 255 <input class="btn btn-link btn-danger" id="remove_${repo_group_name}" name="remove_${repo_group_name}"
256 256 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository group'), _gettext('Delete'), '${_ungettext('`{}` with {} repository','`{}` with {} repositories',gr_count).format(repo_group_name, gr_count)}')"
257 257 type="submit" value="Delete"
258 258 >
259 259 ${h.end_form()}
260 260 </div>
261 261 </%def>
262 262
263 263
264 264 <%def name="user_actions(user_id, username)">
265 265 <div class="grid_edit">
266 266 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
267 267 ${_('Edit')}
268 268 </a>
269 269 </div>
270 270 <div class="grid_delete">
271 271 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
272 272 <input class="btn btn-link btn-danger" id="remove_user_${user_id}" name="remove_user_${user_id}"
273 273 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Delete'), '${username}')"
274 274 type="submit" value="Delete"
275 275 >
276 276 ${h.end_form()}
277 277 </div>
278 278 </%def>
279 279
280 280 <%def name="user_group_actions(user_group_id, user_group_name)">
281 281 <div class="grid_edit">
282 282 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
283 283 </div>
284 284 <div class="grid_delete">
285 285 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
286 286 <input class="btn btn-link btn-danger" id="remove_group_${user_group_id}" name="remove_group_${user_group_id}"
287 287 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user group'), _gettext('Delete'), '${user_group_name}')"
288 288 type="submit" value="Delete"
289 289 >
290 290 ${h.end_form()}
291 291 </div>
292 292 </%def>
293 293
294 294
295 295 <%def name="user_name(user_id, username)">
296 296 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
297 297 </%def>
298 298
299 299 <%def name="user_profile(username)">
300 300 ${base.gravatar_with_user(username, 16, tooltip=True)}
301 301 </%def>
302 302
303 303 <%def name="user_group_name(user_group_name)">
304 304 <div>
305 305 <i class="icon-user-group" title="${_('User group')}"></i>
306 306 ${h.link_to_group(user_group_name)}
307 307 </div>
308 308 </%def>
309 309
310 310
311 311 ## GISTS
312 312
313 313 <%def name="gist_gravatar(full_contact)">
314 314 <div class="gist_gravatar">
315 315 ${base.gravatar(full_contact, 30)}
316 316 </div>
317 317 </%def>
318 318
319 319 <%def name="gist_access_id(gist_access_id, full_contact)">
320 320 <div>
321 321 <code>
322 322 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">${gist_access_id}</a>
323 323 </code>
324 324 </div>
325 325 </%def>
326 326
327 327 <%def name="gist_author(full_contact, created_on, expires)">
328 328 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
329 329 </%def>
330 330
331 331
332 332 <%def name="gist_created(created_on)">
333 333 <div class="created">
334 334 ${h.age_component(created_on, time_is_local=True)}
335 335 </div>
336 336 </%def>
337 337
338 338 <%def name="gist_expires(expires)">
339 339 <div class="created">
340 340 %if expires == -1:
341 341 ${_('never')}
342 342 %else:
343 343 ${h.age_component(h.time_to_utcdatetime(expires))}
344 344 %endif
345 345 </div>
346 346 </%def>
347 347
348 348 <%def name="gist_type(gist_type)">
349 349 %if gist_type == 'public':
350 350 <span class="tag tag-gist-public disabled">${_('Public Gist')}</span>
351 351 %else:
352 352 <span class="tag tag-gist-private disabled">${_('Private Gist')}</span>
353 353 %endif
354 354 </%def>
355 355
356 356 <%def name="gist_description(gist_description)">
357 357 ${gist_description}
358 358 </%def>
359 359
360 360
361 361 ## PULL REQUESTS GRID RENDERERS
362 362
363 363 <%def name="pullrequest_target_repo(repo_name)">
364 364 <div class="truncate">
365 365 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
366 366 </div>
367 367 </%def>
368 368
369 369 <%def name="pullrequest_status(status)">
370 370 <i class="icon-circle review-status-${status}"></i>
371 371 </%def>
372 372
373 373 <%def name="pullrequest_title(title, description)">
374 374 ${title}
375 375 </%def>
376 376
377 377 <%def name="pullrequest_comments(comments_nr)">
378 378 <i class="icon-comment"></i> ${comments_nr}
379 379 </%def>
380 380
381 381 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
382 382 <code>
383 383 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
384 384 % if short:
385 385 !${pull_request_id}
386 386 % else:
387 387 ${_('Pull request !{}').format(pull_request_id)}
388 388 % endif
389 389 </a>
390 390 </code>
391 391 % if state not in ['created']:
392 392 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
393 393 % endif
394 394
395 395 % if is_wip:
396 396 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
397 397 % endif
398 398 </%def>
399 399
400 400 <%def name="pullrequest_updated_on(updated_on)">
401 401 ${h.age_component(h.time_to_utcdatetime(updated_on))}
402 402 </%def>
403 403
404 404 <%def name="pullrequest_author(full_contact)">
405 405 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
406 406 </%def>
407 407
408 408
409 409 ## ARTIFACT RENDERERS
410 410 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
411 411 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
412 412 ${artifact_display_name or '_EMPTY_NAME_'}
413 413 </a>
414 414 </%def>
415 415
416 416 <%def name="repo_artifact_uid(repo_name, file_uid)">
417 417 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
418 418 </%def>
419 419
420 420 <%def name="repo_artifact_sha256(artifact_sha256)">
421 421 <div class="code">${h.shorter(artifact_sha256, 12)}</div>
422 422 </%def>
423 423
424 424 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
425 425 ## <div class="grid_edit">
426 426 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
427 427 ## </div>
428 428 <div class="grid_edit">
429 429 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
430 430 </div>
431 431 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
432 432 <div class="grid_delete">
433 433 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
434 434 <input class="btn btn-link btn-danger" id="remove_artifact_${file_store_id}" name="remove_artifact_${file_store_id}"
435 435 onclick="submitConfirm(event, this, _gettext('Confirm to delete this artifact'), _gettext('Delete'), '${file_uid}')"
436 436 type="submit" value="${_('Delete')}"
437 437 >
438 438 ${h.end_form()}
439 439 </div>
440 440 % endif
441 441 </%def>
442 442
443 443 <%def name="markup_form(form_id, form_text='', help_text=None)">
444 444
445 445 <div class="markup-form">
446 446 <div class="markup-form-area">
447 447 <div class="markup-form-area-header">
448 448 <ul class="nav-links clearfix">
449 449 <li class="active">
450 450 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
451 451 </li>
452 452 <li class="">
453 453 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
454 454 </li>
455 455 </ul>
456 456 </div>
457 457
458 458 <div class="markup-form-area-write" style="display: block;">
459 <div id="edit-container_${form_id}">
459 <div id="edit-container_${form_id}" style="margin-top: -1px">
460 460 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
461 461 </div>
462 462 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
463 463 <div id="preview-box_${form_id}" class="preview-box"></div>
464 464 </div>
465 465 </div>
466 466
467 467 <div class="markup-form-area-footer">
468 468 <div class="toolbar">
469 469 <div class="toolbar-text">
470 470 ${(_('Parsed using %s syntax') % (
471 471 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
472 472 )
473 473 )|n}
474 474 </div>
475 475 </div>
476 476 </div>
477 477 </div>
478 478
479 479 <div class="markup-form-footer">
480 480 % if help_text:
481 481 <span class="help-block">${help_text}</span>
482 482 % endif
483 483 </div>
484 484 </div>
485 485 <script type="text/javascript">
486 486 new MarkupForm('${form_id}');
487 487 </script>
488 488
489 489 </%def>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now