##// END OF EJS Templates
file: new file editors...
dan -
r3754:5cee44bd new-ui
parent child Browse files
Show More
@@ -1,81 +1,85 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.config import routing_links
21 21
22 22
23 23 class VCSCallPredicate(object):
24 24 def __init__(self, val, config):
25 25 self.val = val
26 26
27 27 def text(self):
28 28 return 'vcs_call route = %s' % self.val
29 29
30 30 phash = text
31 31
32 32 def __call__(self, info, request):
33 33 if hasattr(request, 'vcs_call'):
34 34 # skip vcs calls
35 35 return False
36 36
37 37 return True
38 38
39 39
40 40 def includeme(config):
41 41
42 42 config.add_route(
43 43 name='home',
44 44 pattern='/')
45 45
46 46 config.add_route(
47 47 name='user_autocomplete_data',
48 48 pattern='/_users')
49 49
50 50 config.add_route(
51 51 name='user_group_autocomplete_data',
52 52 pattern='/_user_groups')
53 53
54 54 config.add_route(
55 55 name='repo_list_data',
56 56 pattern='/_repos')
57 57
58 58 config.add_route(
59 59 name='repo_group_list_data',
60 60 pattern='/_repo_groups')
61 61
62 62 config.add_route(
63 63 name='goto_switcher_data',
64 64 pattern='/_goto_data')
65 65
66 66 config.add_route(
67 67 name='markup_preview',
68 68 pattern='/_markup_preview')
69 69
70 70 config.add_route(
71 name='file_preview',
72 pattern='/_file_preview')
73
74 config.add_route(
71 75 name='store_user_session_value',
72 76 pattern='/_store_session_attr')
73 77
74 78 # register our static links via redirection mechanism
75 79 routing_links.connect_redirection_links(config)
76 80
77 81 # Scan module for configuration decorators.
78 82 config.scan('.views', ignore='.tests')
79 83
80 84 config.add_route_predicate(
81 85 'skip_vcs_call', VCSCallPredicate)
@@ -1,756 +1,785 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
31 CSRFRequired)
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
32 32 from rhodecode.lib.index import searcher_from_config
33 33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.vcs.nodes import FileNode
35 36 from rhodecode.model.db import (
36 37 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
37 38 from rhodecode.model.repo import RepoModel
38 39 from rhodecode.model.repo_group import RepoGroupModel
39 40 from rhodecode.model.scm import RepoGroupList, RepoList
40 41 from rhodecode.model.user import UserModel
41 42 from rhodecode.model.user_group import UserGroupModel
42 43
43 44 log = logging.getLogger(__name__)
44 45
45 46
46 47 class HomeView(BaseAppView):
47 48
48 49 def load_default_context(self):
49 50 c = self._get_local_tmpl_context()
50 51 c.user = c.auth_user.get_instance()
51 52
52 53 return c
53 54
54 55 @LoginRequired()
55 56 @view_config(
56 57 route_name='user_autocomplete_data', request_method='GET',
57 58 renderer='json_ext', xhr=True)
58 59 def user_autocomplete_data(self):
59 60 self.load_default_context()
60 61 query = self.request.GET.get('query')
61 62 active = str2bool(self.request.GET.get('active') or True)
62 63 include_groups = str2bool(self.request.GET.get('user_groups'))
63 64 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
64 65 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
65 66
66 67 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
67 68 query, active, include_groups)
68 69
69 70 _users = UserModel().get_users(
70 71 name_contains=query, only_active=active)
71 72
72 73 def maybe_skip_default_user(usr):
73 74 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
74 75 return False
75 76 return True
76 77 _users = filter(maybe_skip_default_user, _users)
77 78
78 79 if include_groups:
79 80 # extend with user groups
80 81 _user_groups = UserGroupModel().get_user_groups(
81 82 name_contains=query, only_active=active,
82 83 expand_groups=expand_groups)
83 84 _users = _users + _user_groups
84 85
85 86 return {'suggestions': _users}
86 87
87 88 @LoginRequired()
88 89 @NotAnonymous()
89 90 @view_config(
90 91 route_name='user_group_autocomplete_data', request_method='GET',
91 92 renderer='json_ext', xhr=True)
92 93 def user_group_autocomplete_data(self):
93 94 self.load_default_context()
94 95 query = self.request.GET.get('query')
95 96 active = str2bool(self.request.GET.get('active') or True)
96 97 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
97 98
98 99 log.debug('generating user group list, query:%s, active:%s',
99 100 query, active)
100 101
101 102 _user_groups = UserGroupModel().get_user_groups(
102 103 name_contains=query, only_active=active,
103 104 expand_groups=expand_groups)
104 105 _user_groups = _user_groups
105 106
106 107 return {'suggestions': _user_groups}
107 108
108 109 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
109 110 org_query = name_contains
110 111 allowed_ids = self._rhodecode_user.repo_acl_ids(
111 112 ['repository.read', 'repository.write', 'repository.admin'],
112 113 cache=False, name_filter=name_contains) or [-1]
113 114
114 115 query = Repository.query()\
115 116 .filter(Repository.archived.isnot(true()))\
116 117 .filter(or_(
117 118 # generate multiple IN to fix limitation problems
118 119 *in_filter_generator(Repository.repo_id, allowed_ids)
119 120 ))
120 121
121 122 query = query.order_by(case(
122 123 [
123 124 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
124 125 ],
125 126 ))
126 127 query = query.order_by(func.length(Repository.repo_name))
127 128 query = query.order_by(Repository.repo_name)
128 129
129 130 if repo_type:
130 131 query = query.filter(Repository.repo_type == repo_type)
131 132
132 133 if name_contains:
133 134 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
134 135 query = query.filter(
135 136 Repository.repo_name.ilike(ilike_expression))
136 137 query = query.limit(limit)
137 138
138 139 acl_iter = query
139 140
140 141 return [
141 142 {
142 143 'id': obj.repo_name,
143 144 'value': org_query,
144 145 'value_display': obj.repo_name,
145 146 'text': obj.repo_name,
146 147 'type': 'repo',
147 148 'repo_id': obj.repo_id,
148 149 'repo_type': obj.repo_type,
149 150 'private': obj.private,
150 151 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
151 152 }
152 153 for obj in acl_iter]
153 154
154 155 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
155 156 org_query = name_contains
156 157 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
157 158 ['group.read', 'group.write', 'group.admin'],
158 159 cache=False, name_filter=name_contains) or [-1]
159 160
160 161 query = RepoGroup.query()\
161 162 .filter(or_(
162 163 # generate multiple IN to fix limitation problems
163 164 *in_filter_generator(RepoGroup.group_id, allowed_ids)
164 165 ))
165 166
166 167 query = query.order_by(case(
167 168 [
168 169 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
169 170 ],
170 171 ))
171 172 query = query.order_by(func.length(RepoGroup.group_name))
172 173 query = query.order_by(RepoGroup.group_name)
173 174
174 175 if name_contains:
175 176 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
176 177 query = query.filter(
177 178 RepoGroup.group_name.ilike(ilike_expression))
178 179 query = query.limit(limit)
179 180
180 181 acl_iter = query
181 182
182 183 return [
183 184 {
184 185 'id': obj.group_name,
185 186 'value': org_query,
186 187 'value_display': obj.group_name,
187 188 'text': obj.group_name,
188 189 'type': 'repo_group',
189 190 'repo_group_id': obj.group_id,
190 191 'url': h.route_path(
191 192 'repo_group_home', repo_group_name=obj.group_name)
192 193 }
193 194 for obj in acl_iter]
194 195
195 196 def _get_user_list(self, name_contains=None, limit=20):
196 197 org_query = name_contains
197 198 if not name_contains:
198 199 return [], False
199 200
200 201 # TODO(marcink): should all logged in users be allowed to search others?
201 202 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
202 203 if not allowed_user_search:
203 204 return [], False
204 205
205 206 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
206 207 if len(name_contains) != 1:
207 208 return [], False
208 209
209 210 name_contains = name_contains[0]
210 211
211 212 query = User.query()\
212 213 .order_by(func.length(User.username))\
213 214 .order_by(User.username) \
214 215 .filter(User.username != User.DEFAULT_USER)
215 216
216 217 if name_contains:
217 218 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
218 219 query = query.filter(
219 220 User.username.ilike(ilike_expression))
220 221 query = query.limit(limit)
221 222
222 223 acl_iter = query
223 224
224 225 return [
225 226 {
226 227 'id': obj.user_id,
227 228 'value': org_query,
228 229 'value_display': 'user: `{}`'.format(obj.username),
229 230 'type': 'user',
230 231 'icon_link': h.gravatar_url(obj.email, 30),
231 232 'url': h.route_path(
232 233 'user_profile', username=obj.username)
233 234 }
234 235 for obj in acl_iter], True
235 236
236 237 def _get_user_groups_list(self, name_contains=None, limit=20):
237 238 org_query = name_contains
238 239 if not name_contains:
239 240 return [], False
240 241
241 242 # TODO(marcink): should all logged in users be allowed to search others?
242 243 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
243 244 if not allowed_user_search:
244 245 return [], False
245 246
246 247 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
247 248 if len(name_contains) != 1:
248 249 return [], False
249 250
250 251 name_contains = name_contains[0]
251 252
252 253 query = UserGroup.query()\
253 254 .order_by(func.length(UserGroup.users_group_name))\
254 255 .order_by(UserGroup.users_group_name)
255 256
256 257 if name_contains:
257 258 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
258 259 query = query.filter(
259 260 UserGroup.users_group_name.ilike(ilike_expression))
260 261 query = query.limit(limit)
261 262
262 263 acl_iter = query
263 264
264 265 return [
265 266 {
266 267 'id': obj.users_group_id,
267 268 'value': org_query,
268 269 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
269 270 'type': 'user_group',
270 271 'url': h.route_path(
271 272 'user_group_profile', user_group_name=obj.users_group_name)
272 273 }
273 274 for obj in acl_iter], True
274 275
275 276 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
276 277 repo_name = repo_group_name = None
277 278 if repo:
278 279 repo_name = repo.repo_name
279 280 if repo_group:
280 281 repo_group_name = repo_group.group_name
281 282
282 283 org_query = query
283 284 if not query or len(query) < 3 or not searcher:
284 285 return [], False
285 286
286 287 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
287 288
288 289 if len(commit_hashes) != 1:
289 290 return [], False
290 291
291 292 commit_hash = commit_hashes[0]
292 293
293 294 result = searcher.search(
294 295 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
295 296 repo_name, repo_group_name, raise_on_exc=False)
296 297
297 298 commits = []
298 299 for entry in result['results']:
299 300 repo_data = {
300 301 'repository_id': entry.get('repository_id'),
301 302 'repository_type': entry.get('repo_type'),
302 303 'repository_name': entry.get('repository'),
303 304 }
304 305
305 306 commit_entry = {
306 307 'id': entry['commit_id'],
307 308 'value': org_query,
308 309 'value_display': '`{}` commit: {}'.format(
309 310 entry['repository'], entry['commit_id']),
310 311 'type': 'commit',
311 312 'repo': entry['repository'],
312 313 'repo_data': repo_data,
313 314
314 315 'url': h.route_path(
315 316 'repo_commit',
316 317 repo_name=entry['repository'], commit_id=entry['commit_id'])
317 318 }
318 319
319 320 commits.append(commit_entry)
320 321 return commits, True
321 322
322 323 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
323 324 repo_name = repo_group_name = None
324 325 if repo:
325 326 repo_name = repo.repo_name
326 327 if repo_group:
327 328 repo_group_name = repo_group.group_name
328 329
329 330 org_query = query
330 331 if not query or len(query) < 3 or not searcher:
331 332 return [], False
332 333
333 334 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
334 335 if len(paths_re) != 1:
335 336 return [], False
336 337
337 338 file_path = paths_re[0]
338 339
339 340 search_path = searcher.escape_specials(file_path)
340 341 result = searcher.search(
341 342 'file.raw:*{}*'.format(search_path), 'path', auth_user,
342 343 repo_name, repo_group_name, raise_on_exc=False)
343 344
344 345 files = []
345 346 for entry in result['results']:
346 347 repo_data = {
347 348 'repository_id': entry.get('repository_id'),
348 349 'repository_type': entry.get('repo_type'),
349 350 'repository_name': entry.get('repository'),
350 351 }
351 352
352 353 file_entry = {
353 354 'id': entry['commit_id'],
354 355 'value': org_query,
355 356 'value_display': '`{}` file: {}'.format(
356 357 entry['repository'], entry['file']),
357 358 'type': 'file',
358 359 'repo': entry['repository'],
359 360 'repo_data': repo_data,
360 361
361 362 'url': h.route_path(
362 363 'repo_files',
363 364 repo_name=entry['repository'], commit_id=entry['commit_id'],
364 365 f_path=entry['file'])
365 366 }
366 367
367 368 files.append(file_entry)
368 369 return files, True
369 370
370 371 @LoginRequired()
371 372 @view_config(
372 373 route_name='repo_list_data', request_method='GET',
373 374 renderer='json_ext', xhr=True)
374 375 def repo_list_data(self):
375 376 _ = self.request.translate
376 377 self.load_default_context()
377 378
378 379 query = self.request.GET.get('query')
379 380 repo_type = self.request.GET.get('repo_type')
380 381 log.debug('generating repo list, query:%s, repo_type:%s',
381 382 query, repo_type)
382 383
383 384 res = []
384 385 repos = self._get_repo_list(query, repo_type=repo_type)
385 386 if repos:
386 387 res.append({
387 388 'text': _('Repositories'),
388 389 'children': repos
389 390 })
390 391
391 392 data = {
392 393 'more': False,
393 394 'results': res
394 395 }
395 396 return data
396 397
397 398 @LoginRequired()
398 399 @view_config(
399 400 route_name='repo_group_list_data', request_method='GET',
400 401 renderer='json_ext', xhr=True)
401 402 def repo_group_list_data(self):
402 403 _ = self.request.translate
403 404 self.load_default_context()
404 405
405 406 query = self.request.GET.get('query')
406 407
407 408 log.debug('generating repo group list, query:%s',
408 409 query)
409 410
410 411 res = []
411 412 repo_groups = self._get_repo_group_list(query)
412 413 if repo_groups:
413 414 res.append({
414 415 'text': _('Repository Groups'),
415 416 'children': repo_groups
416 417 })
417 418
418 419 data = {
419 420 'more': False,
420 421 'results': res
421 422 }
422 423 return data
423 424
424 425 def _get_default_search_queries(self, search_context, searcher, query):
425 426 if not searcher:
426 427 return []
427 428
428 429 is_es_6 = searcher.is_es_6
429 430
430 431 queries = []
431 432 repo_group_name, repo_name, repo_context = None, None, None
432 433
433 434 # repo group context
434 435 if search_context.get('search_context[repo_group_name]'):
435 436 repo_group_name = search_context.get('search_context[repo_group_name]')
436 437 if search_context.get('search_context[repo_name]'):
437 438 repo_name = search_context.get('search_context[repo_name]')
438 439 repo_context = search_context.get('search_context[repo_view_type]')
439 440
440 441 if is_es_6 and repo_name:
441 442 # files
442 443 def query_modifier():
443 444 qry = query
444 445 return {'q': qry, 'type': 'content'}
445 446
446 447 label = u'File search for `{}`'.format(h.escape(query))
447 448 file_qry = {
448 449 'id': -10,
449 450 'value': query,
450 451 'value_display': label,
451 452 'type': 'search',
452 453 'subtype': 'repo',
453 454 'url': h.route_path('search_repo',
454 455 repo_name=repo_name,
455 456 _query=query_modifier())
456 457 }
457 458
458 459 # commits
459 460 def query_modifier():
460 461 qry = query
461 462 return {'q': qry, 'type': 'commit'}
462 463
463 464 label = u'Commit search for `{}`'.format(h.escape(query))
464 465 commit_qry = {
465 466 'id': -20,
466 467 'value': query,
467 468 'value_display': label,
468 469 'type': 'search',
469 470 'subtype': 'repo',
470 471 'url': h.route_path('search_repo',
471 472 repo_name=repo_name,
472 473 _query=query_modifier())
473 474 }
474 475
475 476 if repo_context in ['commit', 'commits']:
476 477 queries.extend([commit_qry, file_qry])
477 478 elif repo_context in ['files', 'summary']:
478 479 queries.extend([file_qry, commit_qry])
479 480 else:
480 481 queries.extend([commit_qry, file_qry])
481 482
482 483 elif is_es_6 and repo_group_name:
483 484 # files
484 485 def query_modifier():
485 486 qry = query
486 487 return {'q': qry, 'type': 'content'}
487 488
488 489 label = u'File search for `{}`'.format(query)
489 490 file_qry = {
490 491 'id': -30,
491 492 'value': query,
492 493 'value_display': label,
493 494 'type': 'search',
494 495 'subtype': 'repo_group',
495 496 'url': h.route_path('search_repo_group',
496 497 repo_group_name=repo_group_name,
497 498 _query=query_modifier())
498 499 }
499 500
500 501 # commits
501 502 def query_modifier():
502 503 qry = query
503 504 return {'q': qry, 'type': 'commit'}
504 505
505 506 label = u'Commit search for `{}`'.format(query)
506 507 commit_qry = {
507 508 'id': -40,
508 509 'value': query,
509 510 'value_display': label,
510 511 'type': 'search',
511 512 'subtype': 'repo_group',
512 513 'url': h.route_path('search_repo_group',
513 514 repo_group_name=repo_group_name,
514 515 _query=query_modifier())
515 516 }
516 517
517 518 if repo_context in ['commit', 'commits']:
518 519 queries.extend([commit_qry, file_qry])
519 520 elif repo_context in ['files', 'summary']:
520 521 queries.extend([file_qry, commit_qry])
521 522 else:
522 523 queries.extend([commit_qry, file_qry])
523 524
524 525 # Global, not scoped
525 526 if not queries:
526 527 queries.append(
527 528 {
528 529 'id': -1,
529 530 'value': query,
530 531 'value_display': u'File search for: `{}`'.format(query),
531 532 'type': 'search',
532 533 'subtype': 'global',
533 534 'url': h.route_path('search',
534 535 _query={'q': query, 'type': 'content'})
535 536 })
536 537 queries.append(
537 538 {
538 539 'id': -2,
539 540 'value': query,
540 541 'value_display': u'Commit search for: `{}`'.format(query),
541 542 'type': 'search',
542 543 'subtype': 'global',
543 544 'url': h.route_path('search',
544 545 _query={'q': query, 'type': 'commit'})
545 546 })
546 547
547 548 return queries
548 549
549 550 @LoginRequired()
550 551 @view_config(
551 552 route_name='goto_switcher_data', request_method='GET',
552 553 renderer='json_ext', xhr=True)
553 554 def goto_switcher_data(self):
554 555 c = self.load_default_context()
555 556
556 557 _ = self.request.translate
557 558
558 559 query = self.request.GET.get('query')
559 560 log.debug('generating main filter data, query %s', query)
560 561
561 562 res = []
562 563 if not query:
563 564 return {'suggestions': res}
564 565
565 566 def no_match(name):
566 567 return {
567 568 'id': -1,
568 569 'value': "",
569 570 'value_display': name,
570 571 'type': 'text',
571 572 'url': ""
572 573 }
573 574 searcher = searcher_from_config(self.request.registry.settings)
574 575 has_specialized_search = False
575 576
576 577 # set repo context
577 578 repo = None
578 579 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
579 580 if repo_id:
580 581 repo = Repository.get(repo_id)
581 582
582 583 # set group context
583 584 repo_group = None
584 585 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
585 586 if repo_group_id:
586 587 repo_group = RepoGroup.get(repo_group_id)
587 588 prefix_match = False
588 589
589 590 # user: type search
590 591 if not prefix_match:
591 592 users, prefix_match = self._get_user_list(query)
592 593 if users:
593 594 has_specialized_search = True
594 595 for serialized_user in users:
595 596 res.append(serialized_user)
596 597 elif prefix_match:
597 598 has_specialized_search = True
598 599 res.append(no_match('No matching users found'))
599 600
600 601 # user_group: type search
601 602 if not prefix_match:
602 603 user_groups, prefix_match = self._get_user_groups_list(query)
603 604 if user_groups:
604 605 has_specialized_search = True
605 606 for serialized_user_group in user_groups:
606 607 res.append(serialized_user_group)
607 608 elif prefix_match:
608 609 has_specialized_search = True
609 610 res.append(no_match('No matching user groups found'))
610 611
611 612 # FTS commit: type search
612 613 if not prefix_match:
613 614 commits, prefix_match = self._get_hash_commit_list(
614 615 c.auth_user, searcher, query, repo, repo_group)
615 616 if commits:
616 617 has_specialized_search = True
617 618 unique_repos = collections.OrderedDict()
618 619 for commit in commits:
619 620 repo_name = commit['repo']
620 621 unique_repos.setdefault(repo_name, []).append(commit)
621 622
622 623 for _repo, commits in unique_repos.items():
623 624 for commit in commits:
624 625 res.append(commit)
625 626 elif prefix_match:
626 627 has_specialized_search = True
627 628 res.append(no_match('No matching commits found'))
628 629
629 630 # FTS file: type search
630 631 if not prefix_match:
631 632 paths, prefix_match = self._get_path_list(
632 633 c.auth_user, searcher, query, repo, repo_group)
633 634 if paths:
634 635 has_specialized_search = True
635 636 unique_repos = collections.OrderedDict()
636 637 for path in paths:
637 638 repo_name = path['repo']
638 639 unique_repos.setdefault(repo_name, []).append(path)
639 640
640 641 for repo, paths in unique_repos.items():
641 642 for path in paths:
642 643 res.append(path)
643 644 elif prefix_match:
644 645 has_specialized_search = True
645 646 res.append(no_match('No matching files found'))
646 647
647 648 # main suggestions
648 649 if not has_specialized_search:
649 650 repo_group_name = ''
650 651 if repo_group:
651 652 repo_group_name = repo_group.group_name
652 653
653 654 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
654 655 res.append(_q)
655 656
656 657 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
657 658 for serialized_repo_group in repo_groups:
658 659 res.append(serialized_repo_group)
659 660
660 661 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
661 662 for serialized_repo in repos:
662 663 res.append(serialized_repo)
663 664
664 665 if not repos and not repo_groups:
665 666 res.append(no_match('No matches found'))
666 667
667 668 return {'suggestions': res}
668 669
669 670 def _get_groups_and_repos(self, repo_group_id=None):
670 671 # repo groups groups
671 672 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
672 673 _perms = ['group.read', 'group.write', 'group.admin']
673 674 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
674 675 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
675 676 repo_group_list=repo_group_list_acl, admin=False)
676 677
677 678 # repositories
678 679 repo_list = Repository.get_all_repos(group_id=repo_group_id)
679 680 _perms = ['repository.read', 'repository.write', 'repository.admin']
680 681 repo_list_acl = RepoList(repo_list, perm_set=_perms)
681 682 repo_data = RepoModel().get_repos_as_dict(
682 683 repo_list=repo_list_acl, admin=False)
683 684
684 685 return repo_data, repo_group_data
685 686
686 687 @LoginRequired()
687 688 @view_config(
688 689 route_name='home', request_method='GET',
689 690 renderer='rhodecode:templates/index.mako')
690 691 def main_page(self):
691 692 c = self.load_default_context()
692 693 c.repo_group = None
693 694
694 695 repo_data, repo_group_data = self._get_groups_and_repos()
695 696 # json used to render the grids
696 697 c.repos_data = json.dumps(repo_data)
697 698 c.repo_groups_data = json.dumps(repo_group_data)
698 699
699 700 return self._get_template_context(c)
700 701
701 702 @LoginRequired()
702 703 @HasRepoGroupPermissionAnyDecorator(
703 704 'group.read', 'group.write', 'group.admin')
704 705 @view_config(
705 706 route_name='repo_group_home', request_method='GET',
706 707 renderer='rhodecode:templates/index_repo_group.mako')
707 708 @view_config(
708 709 route_name='repo_group_home_slash', request_method='GET',
709 710 renderer='rhodecode:templates/index_repo_group.mako')
710 711 def repo_group_main_page(self):
711 712 c = self.load_default_context()
712 713 c.repo_group = self.request.db_repo_group
713 714 repo_data, repo_group_data = self._get_groups_and_repos(c.repo_group.group_id)
714 715
715 716 # update every 5 min
716 717 if self.request.db_repo_group.last_commit_cache_update_diff > 60 * 5:
717 718 self.request.db_repo_group.update_commit_cache()
718 719
719 720 # json used to render the grids
720 721 c.repos_data = json.dumps(repo_data)
721 722 c.repo_groups_data = json.dumps(repo_group_data)
722 723
723 724 return self._get_template_context(c)
724 725
725 726 @LoginRequired()
726 727 @CSRFRequired()
727 728 @view_config(
728 729 route_name='markup_preview', request_method='POST',
729 730 renderer='string', xhr=True)
730 731 def markup_preview(self):
731 732 # Technically a CSRF token is not needed as no state changes with this
732 733 # call. However, as this is a POST is better to have it, so automated
733 734 # tools don't flag it as potential CSRF.
734 735 # Post is required because the payload could be bigger than the maximum
735 736 # allowed by GET.
736 737
737 738 text = self.request.POST.get('text')
738 739 renderer = self.request.POST.get('renderer') or 'rst'
739 740 if text:
740 741 return h.render(text, renderer=renderer, mentions=True)
741 742 return ''
742 743
743 744 @LoginRequired()
744 745 @CSRFRequired()
745 746 @view_config(
747 route_name='file_preview', request_method='POST',
748 renderer='string', xhr=True)
749 def file_preview(self):
750 # Technically a CSRF token is not needed as no state changes with this
751 # call. However, as this is a POST is better to have it, so automated
752 # tools don't flag it as potential CSRF.
753 # Post is required because the payload could be bigger than the maximum
754 # allowed by GET.
755
756 text = self.request.POST.get('text')
757 file_path = self.request.POST.get('file_path')
758
759 renderer = h.renderer_from_filename(file_path)
760
761 if renderer:
762 return h.render(text, renderer=renderer, mentions=True)
763 else:
764 self.load_default_context()
765 _render = self.request.get_partial_renderer(
766 'rhodecode:templates/files/file_content.mako')
767
768 lines = filenode_as_lines_tokens(FileNode(file_path, text))
769
770 return _render('render_lines', lines)
771
772 @LoginRequired()
773 @CSRFRequired()
774 @view_config(
746 775 route_name='store_user_session_value', request_method='POST',
747 776 renderer='string', xhr=True)
748 777 def store_user_session_attr(self):
749 778 key = self.request.POST.get('key')
750 779 val = self.request.POST.get('val')
751 780
752 781 existing_value = self.request.session.get(key)
753 782 if existing_value != val:
754 783 self.request.session[key] = val
755 784
756 785 return 'stored:{}:{}'.format(key, val)
@@ -1,1417 +1,1528 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 import pathlib2
28 29
29 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 31 from pyramid.view import view_config
31 32 from pyramid.renderers import render
32 33 from pyramid.response import Response
33 34
34 35 import rhodecode
35 36 from rhodecode.apps._base import RepoAppView
36 37
37 38
38 39 from rhodecode.lib import diffs, helpers as h, rc_cache
39 40 from rhodecode.lib import audit_logger
40 41 from rhodecode.lib.view_utils import parse_path_ref
41 42 from rhodecode.lib.exceptions import NonRelativePathError
42 43 from rhodecode.lib.codeblocks import (
43 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
44 45 from rhodecode.lib.utils2 import (
45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1)
46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
46 47 from rhodecode.lib.auth import (
47 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
48 49 from rhodecode.lib.vcs import path as vcspath
49 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 51 from rhodecode.lib.vcs.conf import settings
51 52 from rhodecode.lib.vcs.nodes import FileNode
52 53 from rhodecode.lib.vcs.exceptions import (
53 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
54 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
55 56 NodeDoesNotExistError, CommitError, NodeError)
56 57
57 58 from rhodecode.model.scm import ScmModel
58 59 from rhodecode.model.db import Repository
59 60
60 61 log = logging.getLogger(__name__)
61 62
62 63
63 64 class RepoFilesView(RepoAppView):
64 65
65 66 @staticmethod
66 67 def adjust_file_path_for_svn(f_path, repo):
67 68 """
68 69 Computes the relative path of `f_path`.
69 70
70 71 This is mainly based on prefix matching of the recognized tags and
71 72 branches in the underlying repository.
72 73 """
73 74 tags_and_branches = itertools.chain(
74 75 repo.branches.iterkeys(),
75 76 repo.tags.iterkeys())
76 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
77 78
78 79 for name in tags_and_branches:
79 80 if f_path.startswith('{}/'.format(name)):
80 81 f_path = vcspath.relpath(f_path, name)
81 82 break
82 83 return f_path
83 84
84 85 def load_default_context(self):
85 86 c = self._get_local_tmpl_context(include_app_defaults=True)
86 87 c.rhodecode_repo = self.rhodecode_vcs_repo
87 88 c.enable_downloads = self.db_repo.enable_downloads
88 89 return c
89 90
90 def _ensure_not_locked(self):
91 def _ensure_not_locked(self, commit_id='tip'):
91 92 _ = self.request.translate
92 93
93 94 repo = self.db_repo
94 95 if repo.enable_locking and repo.locked[0]:
95 96 h.flash(_('This repository has been locked by %s on %s')
96 97 % (h.person_by_id(repo.locked[0]),
97 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
98 99 'warning')
99 100 files_url = h.route_path(
100 101 'repo_files:default_path',
101 repo_name=self.db_repo_name, commit_id='tip')
102 repo_name=self.db_repo_name, commit_id=commit_id)
102 103 raise HTTPFound(files_url)
103 104
104 def check_branch_permission(self, branch_name):
105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 _ = self.request.translate
107
108 if not is_head:
109 message = _('You can only modify files with commit being a valid branch head.')
110 h.flash(message, category='warning')
111
112 if json_mode:
113 return message
114
115 files_url = h.route_path(
116 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
117 f_path=f_path)
118 raise HTTPFound(files_url)
119
120 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
105 121 _ = self.request.translate
106 122
107 123 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
108 124 self.db_repo_name, branch_name)
109 125 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
110 h.flash(
111 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
112 'warning')
126 message = _('Branch `{}` changes forbidden by rule {}.').format(
127 branch_name, rule)
128 h.flash(message, 'warning')
129
130 if json_mode:
131 return message
132
113 133 files_url = h.route_path(
114 'repo_files:default_path',
115 repo_name=self.db_repo_name, commit_id='tip')
134 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
135
116 136 raise HTTPFound(files_url)
117 137
118 138 def _get_commit_and_path(self):
119 139 default_commit_id = self.db_repo.landing_rev[1]
120 140 default_f_path = '/'
121 141
122 142 commit_id = self.request.matchdict.get(
123 143 'commit_id', default_commit_id)
124 144 f_path = self._get_f_path(self.request.matchdict, default_f_path)
125 145 return commit_id, f_path
126 146
127 147 def _get_default_encoding(self, c):
128 148 enc_list = getattr(c, 'default_encodings', [])
129 149 return enc_list[0] if enc_list else 'UTF-8'
130 150
131 151 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
132 152 """
133 153 This is a safe way to get commit. If an error occurs it redirects to
134 154 tip with proper message
135 155
136 156 :param commit_id: id of commit to fetch
137 157 :param redirect_after: toggle redirection
138 158 """
139 159 _ = self.request.translate
140 160
141 161 try:
142 162 return self.rhodecode_vcs_repo.get_commit(commit_id)
143 163 except EmptyRepositoryError:
144 164 if not redirect_after:
145 165 return None
146 166
147 167 _url = h.route_path(
148 168 'repo_files_add_file',
149 repo_name=self.db_repo_name, commit_id=0, f_path='',
150 _anchor='edit')
169 repo_name=self.db_repo_name, commit_id=0, f_path='')
151 170
152 171 if h.HasRepoPermissionAny(
153 172 'repository.write', 'repository.admin')(self.db_repo_name):
154 173 add_new = h.link_to(
155 174 _('Click here to add a new file.'), _url, class_="alert-link")
156 175 else:
157 176 add_new = ""
158 177
159 178 h.flash(h.literal(
160 179 _('There are no files yet. %s') % add_new), category='warning')
161 180 raise HTTPFound(
162 181 h.route_path('repo_summary', repo_name=self.db_repo_name))
163 182
164 183 except (CommitDoesNotExistError, LookupError):
165 184 msg = _('No such commit exists for this repository')
166 185 h.flash(msg, category='error')
167 186 raise HTTPNotFound()
168 187 except RepositoryError as e:
169 188 h.flash(safe_str(h.escape(e)), category='error')
170 189 raise HTTPNotFound()
171 190
172 191 def _get_filenode_or_redirect(self, commit_obj, path):
173 192 """
174 193 Returns file_node, if error occurs or given path is directory,
175 194 it'll redirect to top level path
176 195 """
177 196 _ = self.request.translate
178 197
179 198 try:
180 199 file_node = commit_obj.get_node(path)
181 200 if file_node.is_dir():
182 201 raise RepositoryError('The given path is a directory')
183 202 except CommitDoesNotExistError:
184 203 log.exception('No such commit exists for this repository')
185 204 h.flash(_('No such commit exists for this repository'), category='error')
186 205 raise HTTPNotFound()
187 206 except RepositoryError as e:
188 207 log.warning('Repository error while fetching '
189 208 'filenode `%s`. Err:%s', path, e)
190 209 h.flash(safe_str(h.escape(e)), category='error')
191 210 raise HTTPNotFound()
192 211
193 212 return file_node
194 213
195 214 def _is_valid_head(self, commit_id, repo):
196 215 branch_name = sha_commit_id = ''
197 216 is_head = False
198 217
199 218 if h.is_svn(repo) and not repo.is_empty():
200 219 # Note: Subversion only has one head.
201 220 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
202 221 is_head = True
203 222 return branch_name, sha_commit_id, is_head
204 223
205 224 for _branch_name, branch_commit_id in repo.branches.items():
206 225 # simple case we pass in branch name, it's a HEAD
207 226 if commit_id == _branch_name:
208 227 is_head = True
209 228 branch_name = _branch_name
210 229 sha_commit_id = branch_commit_id
211 230 break
212 231 # case when we pass in full sha commit_id, which is a head
213 232 elif commit_id == branch_commit_id:
214 233 is_head = True
215 234 branch_name = _branch_name
216 235 sha_commit_id = branch_commit_id
217 236 break
218 237
219 238 # checked branches, means we only need to try to get the branch/commit_sha
220 if not repo.is_empty:
239 if not repo.is_empty():
221 240 commit = repo.get_commit(commit_id=commit_id)
222 241 if commit:
223 242 branch_name = commit.branch
224 243 sha_commit_id = commit.raw_id
225 244
226 245 return branch_name, sha_commit_id, is_head
227 246
228 247 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
229 248
230 249 repo_id = self.db_repo.repo_id
231 250 force_recache = self.get_recache_flag()
232 251
233 252 cache_seconds = safe_int(
234 253 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
235 254 cache_on = not force_recache and cache_seconds > 0
236 255 log.debug(
237 256 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
238 257 'with caching: %s[TTL: %ss]' % (
239 258 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
240 259
241 260 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
242 261 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
243 262
244 263 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
245 264 condition=cache_on)
246 265 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
247 266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
248 267 ver, repo_id, commit_id, f_path)
249 268
250 269 c.full_load = full_load
251 270 return render(
252 271 'rhodecode:templates/files/files_browser_tree.mako',
253 272 self._get_template_context(c), self.request)
254 273
255 274 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
256 275
257 276 def _get_archive_spec(self, fname):
258 277 log.debug('Detecting archive spec for: `%s`', fname)
259 278
260 279 fileformat = None
261 280 ext = None
262 281 content_type = None
263 282 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
264 283
265 284 if fname.endswith(extension):
266 285 fileformat = a_type
267 286 log.debug('archive is of type: %s', fileformat)
268 287 ext = extension
269 288 break
270 289
271 290 if not fileformat:
272 291 raise ValueError()
273 292
274 293 # left over part of whole fname is the commit
275 294 commit_id = fname[:-len(ext)]
276 295
277 296 return commit_id, ext, fileformat, content_type
278 297
298 def create_pure_path(self, *parts):
299 # Split paths and sanitize them, removing any ../ etc
300 sanitized_path = [
301 x for x in pathlib2.PurePath(*parts).parts
302 if x not in ['.', '..']]
303
304 pure_path = pathlib2.PurePath(*sanitized_path)
305 return pure_path
306
279 307 @LoginRequired()
280 308 @HasRepoPermissionAnyDecorator(
281 309 'repository.read', 'repository.write', 'repository.admin')
282 310 @view_config(
283 311 route_name='repo_archivefile', request_method='GET',
284 312 renderer=None)
285 313 def repo_archivefile(self):
286 314 # archive cache config
287 315 from rhodecode import CONFIG
288 316 _ = self.request.translate
289 317 self.load_default_context()
290 318 default_at_path = '/'
291 319 fname = self.request.matchdict['fname']
292 320 subrepos = self.request.GET.get('subrepos') == 'true'
293 321 at_path = self.request.GET.get('at_path') or default_at_path
294 322
295 323 if not self.db_repo.enable_downloads:
296 324 return Response(_('Downloads disabled'))
297 325
298 326 try:
299 327 commit_id, ext, fileformat, content_type = \
300 328 self._get_archive_spec(fname)
301 329 except ValueError:
302 330 return Response(_('Unknown archive type for: `{}`').format(
303 331 h.escape(fname)))
304 332
305 333 try:
306 334 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
307 335 except CommitDoesNotExistError:
308 336 return Response(_('Unknown commit_id {}').format(
309 337 h.escape(commit_id)))
310 338 except EmptyRepositoryError:
311 339 return Response(_('Empty repository'))
312 340
313 341 try:
314 342 at_path = commit.get_node(at_path).path or default_at_path
315 343 except Exception:
316 344 return Response(_('No node at path {} for this repository').format(at_path))
317 345
318 346 path_sha = sha1(at_path)[:8]
319 347
320 348 # original backward compat name of archive
321 349 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
322 350 short_sha = safe_str(commit.short_id)
323 351
324 352 if at_path == default_at_path:
325 353 archive_name = '{}-{}{}{}'.format(
326 354 clean_name,
327 355 '-sub' if subrepos else '',
328 356 short_sha,
329 357 ext)
330 358 # custom path and new name
331 359 else:
332 360 archive_name = '{}-{}{}-{}{}'.format(
333 361 clean_name,
334 362 '-sub' if subrepos else '',
335 363 short_sha,
336 364 path_sha,
337 365 ext)
338 366
339 367 use_cached_archive = False
340 368 archive_cache_enabled = CONFIG.get(
341 369 'archive_cache_dir') and not self.request.GET.get('no_cache')
342 370 cached_archive_path = None
343 371
344 372 if archive_cache_enabled:
345 373 # check if we it's ok to write
346 374 if not os.path.isdir(CONFIG['archive_cache_dir']):
347 375 os.makedirs(CONFIG['archive_cache_dir'])
348 376 cached_archive_path = os.path.join(
349 377 CONFIG['archive_cache_dir'], archive_name)
350 378 if os.path.isfile(cached_archive_path):
351 379 log.debug('Found cached archive in %s', cached_archive_path)
352 380 fd, archive = None, cached_archive_path
353 381 use_cached_archive = True
354 382 else:
355 383 log.debug('Archive %s is not yet cached', archive_name)
356 384
357 385 if not use_cached_archive:
358 386 # generate new archive
359 387 fd, archive = tempfile.mkstemp()
360 388 log.debug('Creating new temp archive in %s', archive)
361 389 try:
362 390 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
363 391 archive_at_path=at_path)
364 392 except ImproperArchiveTypeError:
365 393 return _('Unknown archive type')
366 394 if archive_cache_enabled:
367 395 # if we generated the archive and we have cache enabled
368 396 # let's use this for future
369 397 log.debug('Storing new archive in %s', cached_archive_path)
370 398 shutil.move(archive, cached_archive_path)
371 399 archive = cached_archive_path
372 400
373 401 # store download action
374 402 audit_logger.store_web(
375 403 'repo.archive.download', action_data={
376 404 'user_agent': self.request.user_agent,
377 405 'archive_name': archive_name,
378 406 'archive_spec': fname,
379 407 'archive_cached': use_cached_archive},
380 408 user=self._rhodecode_user,
381 409 repo=self.db_repo,
382 410 commit=True
383 411 )
384 412
385 413 def get_chunked_archive(archive_path):
386 414 with open(archive_path, 'rb') as stream:
387 415 while True:
388 416 data = stream.read(16 * 1024)
389 417 if not data:
390 418 if fd: # fd means we used temporary file
391 419 os.close(fd)
392 420 if not archive_cache_enabled:
393 421 log.debug('Destroying temp archive %s', archive_path)
394 422 os.remove(archive_path)
395 423 break
396 424 yield data
397 425
398 426 response = Response(app_iter=get_chunked_archive(archive))
399 427 response.content_disposition = str(
400 428 'attachment; filename=%s' % archive_name)
401 429 response.content_type = str(content_type)
402 430
403 431 return response
404 432
405 433 def _get_file_node(self, commit_id, f_path):
406 434 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
407 435 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
408 436 try:
409 437 node = commit.get_node(f_path)
410 438 if node.is_dir():
411 439 raise NodeError('%s path is a %s not a file'
412 440 % (node, type(node)))
413 441 except NodeDoesNotExistError:
414 442 commit = EmptyCommit(
415 443 commit_id=commit_id,
416 444 idx=commit.idx,
417 445 repo=commit.repository,
418 446 alias=commit.repository.alias,
419 447 message=commit.message,
420 448 author=commit.author,
421 449 date=commit.date)
422 450 node = FileNode(f_path, '', commit=commit)
423 451 else:
424 452 commit = EmptyCommit(
425 453 repo=self.rhodecode_vcs_repo,
426 454 alias=self.rhodecode_vcs_repo.alias)
427 455 node = FileNode(f_path, '', commit=commit)
428 456 return node
429 457
430 458 @LoginRequired()
431 459 @HasRepoPermissionAnyDecorator(
432 460 'repository.read', 'repository.write', 'repository.admin')
433 461 @view_config(
434 462 route_name='repo_files_diff', request_method='GET',
435 463 renderer=None)
436 464 def repo_files_diff(self):
437 465 c = self.load_default_context()
438 466 f_path = self._get_f_path(self.request.matchdict)
439 467 diff1 = self.request.GET.get('diff1', '')
440 468 diff2 = self.request.GET.get('diff2', '')
441 469
442 470 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
443 471
444 472 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
445 473 line_context = self.request.GET.get('context', 3)
446 474
447 475 if not any((diff1, diff2)):
448 476 h.flash(
449 477 'Need query parameter "diff1" or "diff2" to generate a diff.',
450 478 category='error')
451 479 raise HTTPBadRequest()
452 480
453 481 c.action = self.request.GET.get('diff')
454 482 if c.action not in ['download', 'raw']:
455 483 compare_url = h.route_path(
456 484 'repo_compare',
457 485 repo_name=self.db_repo_name,
458 486 source_ref_type='rev',
459 487 source_ref=diff1,
460 488 target_repo=self.db_repo_name,
461 489 target_ref_type='rev',
462 490 target_ref=diff2,
463 491 _query=dict(f_path=f_path))
464 492 # redirect to new view if we render diff
465 493 raise HTTPFound(compare_url)
466 494
467 495 try:
468 496 node1 = self._get_file_node(diff1, path1)
469 497 node2 = self._get_file_node(diff2, f_path)
470 498 except (RepositoryError, NodeError):
471 499 log.exception("Exception while trying to get node from repository")
472 500 raise HTTPFound(
473 501 h.route_path('repo_files', repo_name=self.db_repo_name,
474 502 commit_id='tip', f_path=f_path))
475 503
476 504 if all(isinstance(node.commit, EmptyCommit)
477 505 for node in (node1, node2)):
478 506 raise HTTPNotFound()
479 507
480 508 c.commit_1 = node1.commit
481 509 c.commit_2 = node2.commit
482 510
483 511 if c.action == 'download':
484 512 _diff = diffs.get_gitdiff(node1, node2,
485 513 ignore_whitespace=ignore_whitespace,
486 514 context=line_context)
487 515 diff = diffs.DiffProcessor(_diff, format='gitdiff')
488 516
489 517 response = Response(self.path_filter.get_raw_patch(diff))
490 518 response.content_type = 'text/plain'
491 519 response.content_disposition = (
492 520 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
493 521 )
494 522 charset = self._get_default_encoding(c)
495 523 if charset:
496 524 response.charset = charset
497 525 return response
498 526
499 527 elif c.action == 'raw':
500 528 _diff = diffs.get_gitdiff(node1, node2,
501 529 ignore_whitespace=ignore_whitespace,
502 530 context=line_context)
503 531 diff = diffs.DiffProcessor(_diff, format='gitdiff')
504 532
505 533 response = Response(self.path_filter.get_raw_patch(diff))
506 534 response.content_type = 'text/plain'
507 535 charset = self._get_default_encoding(c)
508 536 if charset:
509 537 response.charset = charset
510 538 return response
511 539
512 540 # in case we ever end up here
513 541 raise HTTPNotFound()
514 542
515 543 @LoginRequired()
516 544 @HasRepoPermissionAnyDecorator(
517 545 'repository.read', 'repository.write', 'repository.admin')
518 546 @view_config(
519 547 route_name='repo_files_diff_2way_redirect', request_method='GET',
520 548 renderer=None)
521 549 def repo_files_diff_2way_redirect(self):
522 550 """
523 551 Kept only to make OLD links work
524 552 """
525 553 f_path = self._get_f_path_unchecked(self.request.matchdict)
526 554 diff1 = self.request.GET.get('diff1', '')
527 555 diff2 = self.request.GET.get('diff2', '')
528 556
529 557 if not any((diff1, diff2)):
530 558 h.flash(
531 559 'Need query parameter "diff1" or "diff2" to generate a diff.',
532 560 category='error')
533 561 raise HTTPBadRequest()
534 562
535 563 compare_url = h.route_path(
536 564 'repo_compare',
537 565 repo_name=self.db_repo_name,
538 566 source_ref_type='rev',
539 567 source_ref=diff1,
540 568 target_ref_type='rev',
541 569 target_ref=diff2,
542 570 _query=dict(f_path=f_path, diffmode='sideside',
543 571 target_repo=self.db_repo_name,))
544 572 raise HTTPFound(compare_url)
545 573
546 574 @LoginRequired()
547 575 @HasRepoPermissionAnyDecorator(
548 576 'repository.read', 'repository.write', 'repository.admin')
549 577 @view_config(
550 578 route_name='repo_files', request_method='GET',
551 579 renderer=None)
552 580 @view_config(
553 581 route_name='repo_files:default_path', request_method='GET',
554 582 renderer=None)
555 583 @view_config(
556 584 route_name='repo_files:default_commit', request_method='GET',
557 585 renderer=None)
558 586 @view_config(
559 587 route_name='repo_files:rendered', request_method='GET',
560 588 renderer=None)
561 589 @view_config(
562 590 route_name='repo_files:annotated', request_method='GET',
563 591 renderer=None)
564 592 def repo_files(self):
565 593 c = self.load_default_context()
566 594
567 595 view_name = getattr(self.request.matched_route, 'name', None)
568 596
569 597 c.annotate = view_name == 'repo_files:annotated'
570 598 # default is false, but .rst/.md files later are auto rendered, we can
571 599 # overwrite auto rendering by setting this GET flag
572 600 c.renderer = view_name == 'repo_files:rendered' or \
573 601 not self.request.GET.get('no-render', False)
574 602
575 603 # redirect to given commit_id from form if given
576 604 get_commit_id = self.request.GET.get('at_rev', None)
577 605 if get_commit_id:
578 606 self._get_commit_or_redirect(get_commit_id)
579 607
580 608 commit_id, f_path = self._get_commit_and_path()
581 609 c.commit = self._get_commit_or_redirect(commit_id)
582 610 c.branch = self.request.GET.get('branch', None)
583 611 c.f_path = f_path
584 612
585 613 # prev link
586 614 try:
587 615 prev_commit = c.commit.prev(c.branch)
588 616 c.prev_commit = prev_commit
589 617 c.url_prev = h.route_path(
590 618 'repo_files', repo_name=self.db_repo_name,
591 619 commit_id=prev_commit.raw_id, f_path=f_path)
592 620 if c.branch:
593 621 c.url_prev += '?branch=%s' % c.branch
594 622 except (CommitDoesNotExistError, VCSError):
595 623 c.url_prev = '#'
596 624 c.prev_commit = EmptyCommit()
597 625
598 626 # next link
599 627 try:
600 628 next_commit = c.commit.next(c.branch)
601 629 c.next_commit = next_commit
602 630 c.url_next = h.route_path(
603 631 'repo_files', repo_name=self.db_repo_name,
604 632 commit_id=next_commit.raw_id, f_path=f_path)
605 633 if c.branch:
606 634 c.url_next += '?branch=%s' % c.branch
607 635 except (CommitDoesNotExistError, VCSError):
608 636 c.url_next = '#'
609 637 c.next_commit = EmptyCommit()
610 638
611 639 # files or dirs
612 640 try:
613 641 c.file = c.commit.get_node(f_path)
614 642 c.file_author = True
615 643 c.file_tree = ''
616 644
617 645 # load file content
618 646 if c.file.is_file():
619 647 c.lf_node = c.file.get_largefile_node()
620 648
621 649 c.file_source_page = 'true'
622 650 c.file_last_commit = c.file.last_commit
623 651 if c.file.size < c.visual.cut_off_limit_diff:
624 652 if c.annotate: # annotation has precedence over renderer
625 653 c.annotated_lines = filenode_as_annotated_lines_tokens(
626 654 c.file
627 655 )
628 656 else:
629 657 c.renderer = (
630 658 c.renderer and h.renderer_from_filename(c.file.path)
631 659 )
632 660 if not c.renderer:
633 661 c.lines = filenode_as_lines_tokens(c.file)
634 662
635 663 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
636 664 commit_id, self.rhodecode_vcs_repo)
637 665 c.on_branch_head = is_head
638 666
639 667 branch = c.commit.branch if (
640 668 c.commit.branch and '/' not in c.commit.branch) else None
641 669 c.branch_or_raw_id = branch or c.commit.raw_id
642 670 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
643 671
644 672 author = c.file_last_commit.author
645 673 c.authors = [[
646 674 h.email(author),
647 675 h.person(author, 'username_or_name_or_email'),
648 676 1
649 677 ]]
650 678
651 679 else: # load tree content at path
652 680 c.file_source_page = 'false'
653 681 c.authors = []
654 682 # this loads a simple tree without metadata to speed things up
655 683 # later via ajax we call repo_nodetree_full and fetch whole
656 684 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
657 685
658 686 except RepositoryError as e:
659 687 h.flash(safe_str(h.escape(e)), category='error')
660 688 raise HTTPNotFound()
661 689
662 690 if self.request.environ.get('HTTP_X_PJAX'):
663 691 html = render('rhodecode:templates/files/files_pjax.mako',
664 692 self._get_template_context(c), self.request)
665 693 else:
666 694 html = render('rhodecode:templates/files/files.mako',
667 695 self._get_template_context(c), self.request)
668 696 return Response(html)
669 697
670 698 @HasRepoPermissionAnyDecorator(
671 699 'repository.read', 'repository.write', 'repository.admin')
672 700 @view_config(
673 701 route_name='repo_files:annotated_previous', request_method='GET',
674 702 renderer=None)
675 703 def repo_files_annotated_previous(self):
676 704 self.load_default_context()
677 705
678 706 commit_id, f_path = self._get_commit_and_path()
679 707 commit = self._get_commit_or_redirect(commit_id)
680 708 prev_commit_id = commit.raw_id
681 709 line_anchor = self.request.GET.get('line_anchor')
682 710 is_file = False
683 711 try:
684 712 _file = commit.get_node(f_path)
685 713 is_file = _file.is_file()
686 714 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
687 715 pass
688 716
689 717 if is_file:
690 718 history = commit.get_path_history(f_path)
691 719 prev_commit_id = history[1].raw_id \
692 720 if len(history) > 1 else prev_commit_id
693 721 prev_url = h.route_path(
694 722 'repo_files:annotated', repo_name=self.db_repo_name,
695 723 commit_id=prev_commit_id, f_path=f_path,
696 724 _anchor='L{}'.format(line_anchor))
697 725
698 726 raise HTTPFound(prev_url)
699 727
700 728 @LoginRequired()
701 729 @HasRepoPermissionAnyDecorator(
702 730 'repository.read', 'repository.write', 'repository.admin')
703 731 @view_config(
704 732 route_name='repo_nodetree_full', request_method='GET',
705 733 renderer=None, xhr=True)
706 734 @view_config(
707 735 route_name='repo_nodetree_full:default_path', request_method='GET',
708 736 renderer=None, xhr=True)
709 737 def repo_nodetree_full(self):
710 738 """
711 739 Returns rendered html of file tree that contains commit date,
712 740 author, commit_id for the specified combination of
713 741 repo, commit_id and file path
714 742 """
715 743 c = self.load_default_context()
716 744
717 745 commit_id, f_path = self._get_commit_and_path()
718 746 commit = self._get_commit_or_redirect(commit_id)
719 747 try:
720 748 dir_node = commit.get_node(f_path)
721 749 except RepositoryError as e:
722 750 return Response('error: {}'.format(h.escape(safe_str(e))))
723 751
724 752 if dir_node.is_file():
725 753 return Response('')
726 754
727 755 c.file = dir_node
728 756 c.commit = commit
729 757
730 758 html = self._get_tree_at_commit(
731 759 c, commit.raw_id, dir_node.path, full_load=True)
732 760
733 761 return Response(html)
734 762
735 763 def _get_attachement_headers(self, f_path):
736 764 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
737 765 safe_path = f_name.replace('"', '\\"')
738 766 encoded_path = urllib.quote(f_name)
739 767
740 768 return "attachment; " \
741 769 "filename=\"{}\"; " \
742 770 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
743 771
744 772 @LoginRequired()
745 773 @HasRepoPermissionAnyDecorator(
746 774 'repository.read', 'repository.write', 'repository.admin')
747 775 @view_config(
748 776 route_name='repo_file_raw', request_method='GET',
749 777 renderer=None)
750 778 def repo_file_raw(self):
751 779 """
752 780 Action for show as raw, some mimetypes are "rendered",
753 781 those include images, icons.
754 782 """
755 783 c = self.load_default_context()
756 784
757 785 commit_id, f_path = self._get_commit_and_path()
758 786 commit = self._get_commit_or_redirect(commit_id)
759 787 file_node = self._get_filenode_or_redirect(commit, f_path)
760 788
761 789 raw_mimetype_mapping = {
762 790 # map original mimetype to a mimetype used for "show as raw"
763 791 # you can also provide a content-disposition to override the
764 792 # default "attachment" disposition.
765 793 # orig_type: (new_type, new_dispo)
766 794
767 795 # show images inline:
768 796 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
769 797 # for example render an SVG with javascript inside or even render
770 798 # HTML.
771 799 'image/x-icon': ('image/x-icon', 'inline'),
772 800 'image/png': ('image/png', 'inline'),
773 801 'image/gif': ('image/gif', 'inline'),
774 802 'image/jpeg': ('image/jpeg', 'inline'),
775 803 'application/pdf': ('application/pdf', 'inline'),
776 804 }
777 805
778 806 mimetype = file_node.mimetype
779 807 try:
780 808 mimetype, disposition = raw_mimetype_mapping[mimetype]
781 809 except KeyError:
782 810 # we don't know anything special about this, handle it safely
783 811 if file_node.is_binary:
784 812 # do same as download raw for binary files
785 813 mimetype, disposition = 'application/octet-stream', 'attachment'
786 814 else:
787 815 # do not just use the original mimetype, but force text/plain,
788 816 # otherwise it would serve text/html and that might be unsafe.
789 817 # Note: underlying vcs library fakes text/plain mimetype if the
790 818 # mimetype can not be determined and it thinks it is not
791 819 # binary.This might lead to erroneous text display in some
792 820 # cases, but helps in other cases, like with text files
793 821 # without extension.
794 822 mimetype, disposition = 'text/plain', 'inline'
795 823
796 824 if disposition == 'attachment':
797 825 disposition = self._get_attachement_headers(f_path)
798 826
799 827 def stream_node():
800 828 yield file_node.raw_bytes
801 829
802 830 response = Response(app_iter=stream_node())
803 831 response.content_disposition = disposition
804 832 response.content_type = mimetype
805 833
806 834 charset = self._get_default_encoding(c)
807 835 if charset:
808 836 response.charset = charset
809 837
810 838 return response
811 839
812 840 @LoginRequired()
813 841 @HasRepoPermissionAnyDecorator(
814 842 'repository.read', 'repository.write', 'repository.admin')
815 843 @view_config(
816 844 route_name='repo_file_download', request_method='GET',
817 845 renderer=None)
818 846 @view_config(
819 847 route_name='repo_file_download:legacy', request_method='GET',
820 848 renderer=None)
821 849 def repo_file_download(self):
822 850 c = self.load_default_context()
823 851
824 852 commit_id, f_path = self._get_commit_and_path()
825 853 commit = self._get_commit_or_redirect(commit_id)
826 854 file_node = self._get_filenode_or_redirect(commit, f_path)
827 855
828 856 if self.request.GET.get('lf'):
829 857 # only if lf get flag is passed, we download this file
830 858 # as LFS/Largefile
831 859 lf_node = file_node.get_largefile_node()
832 860 if lf_node:
833 861 # overwrite our pointer with the REAL large-file
834 862 file_node = lf_node
835 863
836 864 disposition = self._get_attachement_headers(f_path)
837 865
838 866 def stream_node():
839 867 yield file_node.raw_bytes
840 868
841 869 response = Response(app_iter=stream_node())
842 870 response.content_disposition = disposition
843 871 response.content_type = file_node.mimetype
844 872
845 873 charset = self._get_default_encoding(c)
846 874 if charset:
847 875 response.charset = charset
848 876
849 877 return response
850 878
851 879 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
852 880
853 881 cache_seconds = safe_int(
854 882 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
855 883 cache_on = cache_seconds > 0
856 884 log.debug(
857 885 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
858 886 'with caching: %s[TTL: %ss]' % (
859 887 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
860 888
861 889 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
862 890 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
863 891
864 892 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
865 893 condition=cache_on)
866 894 def compute_file_search(repo_id, commit_id, f_path):
867 895 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
868 896 repo_id, commit_id, f_path)
869 897 try:
870 898 _d, _f = ScmModel().get_nodes(
871 899 repo_name, commit_id, f_path, flat=False)
872 900 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
873 901 log.exception(safe_str(e))
874 902 h.flash(safe_str(h.escape(e)), category='error')
875 903 raise HTTPFound(h.route_path(
876 904 'repo_files', repo_name=self.db_repo_name,
877 905 commit_id='tip', f_path='/'))
878 906 return _d + _f
879 907
880 908 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
881 909
882 910 @LoginRequired()
883 911 @HasRepoPermissionAnyDecorator(
884 912 'repository.read', 'repository.write', 'repository.admin')
885 913 @view_config(
886 914 route_name='repo_files_nodelist', request_method='GET',
887 915 renderer='json_ext', xhr=True)
888 916 def repo_nodelist(self):
889 917 self.load_default_context()
890 918
891 919 commit_id, f_path = self._get_commit_and_path()
892 920 commit = self._get_commit_or_redirect(commit_id)
893 921
894 922 metadata = self._get_nodelist_at_commit(
895 923 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
896 924 return {'nodes': metadata}
897 925
898 926 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
899 927 items = []
900 928 for name, commit_id in branches_or_tags.items():
901 929 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
902 930 items.append((sym_ref, name, ref_type))
903 931 return items
904 932
905 933 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
906 934 return commit_id
907 935
908 936 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
909 937 new_f_path = vcspath.join(name, f_path)
910 938 return u'%s@%s' % (new_f_path, commit_id)
911 939
912 940 def _get_node_history(self, commit_obj, f_path, commits=None):
913 941 """
914 942 get commit history for given node
915 943
916 944 :param commit_obj: commit to calculate history
917 945 :param f_path: path for node to calculate history for
918 946 :param commits: if passed don't calculate history and take
919 947 commits defined in this list
920 948 """
921 949 _ = self.request.translate
922 950
923 951 # calculate history based on tip
924 952 tip = self.rhodecode_vcs_repo.get_commit()
925 953 if commits is None:
926 954 pre_load = ["author", "branch"]
927 955 try:
928 956 commits = tip.get_path_history(f_path, pre_load=pre_load)
929 957 except (NodeDoesNotExistError, CommitError):
930 958 # this node is not present at tip!
931 959 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
932 960
933 961 history = []
934 962 commits_group = ([], _("Changesets"))
935 963 for commit in commits:
936 964 branch = ' (%s)' % commit.branch if commit.branch else ''
937 965 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
938 966 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
939 967 history.append(commits_group)
940 968
941 969 symbolic_reference = self._symbolic_reference
942 970
943 971 if self.rhodecode_vcs_repo.alias == 'svn':
944 972 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
945 973 f_path, self.rhodecode_vcs_repo)
946 974 if adjusted_f_path != f_path:
947 975 log.debug(
948 976 'Recognized svn tag or branch in file "%s", using svn '
949 977 'specific symbolic references', f_path)
950 978 f_path = adjusted_f_path
951 979 symbolic_reference = self._symbolic_reference_svn
952 980
953 981 branches = self._create_references(
954 982 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
955 983 branches_group = (branches, _("Branches"))
956 984
957 985 tags = self._create_references(
958 986 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
959 987 tags_group = (tags, _("Tags"))
960 988
961 989 history.append(branches_group)
962 990 history.append(tags_group)
963 991
964 992 return history, commits
965 993
966 994 @LoginRequired()
967 995 @HasRepoPermissionAnyDecorator(
968 996 'repository.read', 'repository.write', 'repository.admin')
969 997 @view_config(
970 998 route_name='repo_file_history', request_method='GET',
971 999 renderer='json_ext')
972 1000 def repo_file_history(self):
973 1001 self.load_default_context()
974 1002
975 1003 commit_id, f_path = self._get_commit_and_path()
976 1004 commit = self._get_commit_or_redirect(commit_id)
977 1005 file_node = self._get_filenode_or_redirect(commit, f_path)
978 1006
979 1007 if file_node.is_file():
980 1008 file_history, _hist = self._get_node_history(commit, f_path)
981 1009
982 1010 res = []
983 1011 for obj in file_history:
984 1012 res.append({
985 1013 'text': obj[1],
986 1014 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
987 1015 })
988 1016
989 1017 data = {
990 1018 'more': False,
991 1019 'results': res
992 1020 }
993 1021 return data
994 1022
995 1023 log.warning('Cannot fetch history for directory')
996 1024 raise HTTPBadRequest()
997 1025
998 1026 @LoginRequired()
999 1027 @HasRepoPermissionAnyDecorator(
1000 1028 'repository.read', 'repository.write', 'repository.admin')
1001 1029 @view_config(
1002 1030 route_name='repo_file_authors', request_method='GET',
1003 1031 renderer='rhodecode:templates/files/file_authors_box.mako')
1004 1032 def repo_file_authors(self):
1005 1033 c = self.load_default_context()
1006 1034
1007 1035 commit_id, f_path = self._get_commit_and_path()
1008 1036 commit = self._get_commit_or_redirect(commit_id)
1009 1037 file_node = self._get_filenode_or_redirect(commit, f_path)
1010 1038
1011 1039 if not file_node.is_file():
1012 1040 raise HTTPBadRequest()
1013 1041
1014 1042 c.file_last_commit = file_node.last_commit
1015 1043 if self.request.GET.get('annotate') == '1':
1016 1044 # use _hist from annotation if annotation mode is on
1017 1045 commit_ids = set(x[1] for x in file_node.annotate)
1018 1046 _hist = (
1019 1047 self.rhodecode_vcs_repo.get_commit(commit_id)
1020 1048 for commit_id in commit_ids)
1021 1049 else:
1022 1050 _f_history, _hist = self._get_node_history(commit, f_path)
1023 1051 c.file_author = False
1024 1052
1025 1053 unique = collections.OrderedDict()
1026 1054 for commit in _hist:
1027 1055 author = commit.author
1028 1056 if author not in unique:
1029 1057 unique[commit.author] = [
1030 1058 h.email(author),
1031 1059 h.person(author, 'username_or_name_or_email'),
1032 1060 1 # counter
1033 1061 ]
1034 1062
1035 1063 else:
1036 1064 # increase counter
1037 1065 unique[commit.author][2] += 1
1038 1066
1039 1067 c.authors = [val for val in unique.values()]
1040 1068
1041 1069 return self._get_template_context(c)
1042 1070
1043 1071 @LoginRequired()
1044 1072 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1045 1073 @view_config(
1046 1074 route_name='repo_files_remove_file', request_method='GET',
1047 1075 renderer='rhodecode:templates/files/files_delete.mako')
1048 1076 def repo_files_remove_file(self):
1049 1077 _ = self.request.translate
1050 1078 c = self.load_default_context()
1051 1079 commit_id, f_path = self._get_commit_and_path()
1052 1080
1053 1081 self._ensure_not_locked()
1054 1082 _branch_name, _sha_commit_id, is_head = \
1055 1083 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1056 1084
1057 if not is_head:
1058 h.flash(_('You can only delete files with commit '
1059 'being a valid branch head.'), category='warning')
1060 raise HTTPFound(
1061 h.route_path('repo_files',
1062 repo_name=self.db_repo_name, commit_id='tip',
1063 f_path=f_path))
1085 self.forbid_non_head(is_head, f_path)
1086 self.check_branch_permission(_branch_name)
1064 1087
1065 self.check_branch_permission(_branch_name)
1066 1088 c.commit = self._get_commit_or_redirect(commit_id)
1067 1089 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1068 1090
1069 1091 c.default_message = _(
1070 1092 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1071 1093 c.f_path = f_path
1072 1094
1073 1095 return self._get_template_context(c)
1074 1096
1075 1097 @LoginRequired()
1076 1098 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1077 1099 @CSRFRequired()
1078 1100 @view_config(
1079 1101 route_name='repo_files_delete_file', request_method='POST',
1080 1102 renderer=None)
1081 1103 def repo_files_delete_file(self):
1082 1104 _ = self.request.translate
1083 1105
1084 1106 c = self.load_default_context()
1085 1107 commit_id, f_path = self._get_commit_and_path()
1086 1108
1087 1109 self._ensure_not_locked()
1088 1110 _branch_name, _sha_commit_id, is_head = \
1089 1111 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1090 1112
1091 if not is_head:
1092 h.flash(_('You can only delete files with commit '
1093 'being a valid branch head.'), category='warning')
1094 raise HTTPFound(
1095 h.route_path('repo_files',
1096 repo_name=self.db_repo_name, commit_id='tip',
1097 f_path=f_path))
1113 self.forbid_non_head(is_head, f_path)
1098 1114 self.check_branch_permission(_branch_name)
1099 1115
1100 1116 c.commit = self._get_commit_or_redirect(commit_id)
1101 1117 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1102 1118
1103 1119 c.default_message = _(
1104 1120 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1105 1121 c.f_path = f_path
1106 1122 node_path = f_path
1107 1123 author = self._rhodecode_db_user.full_contact
1108 1124 message = self.request.POST.get('message') or c.default_message
1109 1125 try:
1110 1126 nodes = {
1111 1127 node_path: {
1112 1128 'content': ''
1113 1129 }
1114 1130 }
1115 1131 ScmModel().delete_nodes(
1116 1132 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1117 1133 message=message,
1118 1134 nodes=nodes,
1119 1135 parent_commit=c.commit,
1120 1136 author=author,
1121 1137 )
1122 1138
1123 1139 h.flash(
1124 1140 _('Successfully deleted file `{}`').format(
1125 1141 h.escape(f_path)), category='success')
1126 1142 except Exception:
1127 1143 log.exception('Error during commit operation')
1128 1144 h.flash(_('Error occurred during commit'), category='error')
1129 1145 raise HTTPFound(
1130 1146 h.route_path('repo_commit', repo_name=self.db_repo_name,
1131 1147 commit_id='tip'))
1132 1148
1133 1149 @LoginRequired()
1134 1150 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1135 1151 @view_config(
1136 1152 route_name='repo_files_edit_file', request_method='GET',
1137 1153 renderer='rhodecode:templates/files/files_edit.mako')
1138 1154 def repo_files_edit_file(self):
1139 1155 _ = self.request.translate
1140 1156 c = self.load_default_context()
1141 1157 commit_id, f_path = self._get_commit_and_path()
1142 1158
1143 1159 self._ensure_not_locked()
1144 1160 _branch_name, _sha_commit_id, is_head = \
1145 1161 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1146 1162
1147 if not is_head:
1148 h.flash(_('You can only edit files with commit '
1149 'being a valid branch head.'), category='warning')
1150 raise HTTPFound(
1151 h.route_path('repo_files',
1152 repo_name=self.db_repo_name, commit_id='tip',
1153 f_path=f_path))
1154 self.check_branch_permission(_branch_name)
1163 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1164 self.check_branch_permission(_branch_name, commit_id=commit_id)
1155 1165
1156 1166 c.commit = self._get_commit_or_redirect(commit_id)
1157 1167 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1158 1168
1159 1169 if c.file.is_binary:
1160 1170 files_url = h.route_path(
1161 1171 'repo_files',
1162 1172 repo_name=self.db_repo_name,
1163 1173 commit_id=c.commit.raw_id, f_path=f_path)
1164 1174 raise HTTPFound(files_url)
1165 1175
1166 c.default_message = _(
1167 'Edited file {} via RhodeCode Enterprise').format(f_path)
1176 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1168 1177 c.f_path = f_path
1169 1178
1170 1179 return self._get_template_context(c)
1171 1180
1172 1181 @LoginRequired()
1173 1182 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1174 1183 @CSRFRequired()
1175 1184 @view_config(
1176 1185 route_name='repo_files_update_file', request_method='POST',
1177 1186 renderer=None)
1178 1187 def repo_files_update_file(self):
1179 1188 _ = self.request.translate
1180 1189 c = self.load_default_context()
1181 1190 commit_id, f_path = self._get_commit_and_path()
1182 1191
1183 1192 self._ensure_not_locked()
1184 _branch_name, _sha_commit_id, is_head = \
1185 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1186
1187 if not is_head:
1188 h.flash(_('You can only edit files with commit '
1189 'being a valid branch head.'), category='warning')
1190 raise HTTPFound(
1191 h.route_path('repo_files',
1192 repo_name=self.db_repo_name, commit_id='tip',
1193 f_path=f_path))
1194
1195 self.check_branch_permission(_branch_name)
1196 1193
1197 1194 c.commit = self._get_commit_or_redirect(commit_id)
1198 1195 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1199 1196
1200 1197 if c.file.is_binary:
1201 raise HTTPFound(
1202 h.route_path('repo_files',
1203 repo_name=self.db_repo_name,
1204 commit_id=c.commit.raw_id,
1205 f_path=f_path))
1198 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1199 commit_id=c.commit.raw_id, f_path=f_path))
1200
1201 _branch_name, _sha_commit_id, is_head = \
1202 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1206 1203
1207 c.default_message = _(
1208 'Edited file {} via RhodeCode Enterprise').format(f_path)
1204 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1205 self.check_branch_permission(_branch_name, commit_id=commit_id)
1206
1207 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1209 1208 c.f_path = f_path
1209
1210 1210 old_content = c.file.content
1211 1211 sl = old_content.splitlines(1)
1212 1212 first_line = sl[0] if sl else ''
1213 1213
1214 1214 r_post = self.request.POST
1215 1215 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1216 1216 line_ending_mode = detect_mode(first_line, 0)
1217 1217 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1218 1218
1219 1219 message = r_post.get('message') or c.default_message
1220 org_f_path = c.file.unicode_path
1220 org_node_path = c.file.unicode_path
1221 1221 filename = r_post['filename']
1222 org_filename = c.file.name
1222
1223 root_path = c.file.dir_path
1224 pure_path = self.create_pure_path(root_path, filename)
1225 node_path = safe_unicode(bytes(pure_path))
1223 1226
1224 if content == old_content and filename == org_filename:
1225 h.flash(_('No changes'), category='warning')
1226 raise HTTPFound(
1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1228 commit_id='tip'))
1227 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1228 commit_id=commit_id)
1229 if content == old_content and node_path == org_node_path:
1230 h.flash(_('No changes detected on {}').format(org_node_path),
1231 category='warning')
1232 raise HTTPFound(default_redirect_url)
1233
1229 1234 try:
1230 1235 mapping = {
1231 org_f_path: {
1232 'org_filename': org_f_path,
1233 'filename': os.path.join(c.file.dir_path, filename),
1236 org_node_path: {
1237 'org_filename': org_node_path,
1238 'filename': node_path,
1234 1239 'content': content,
1235 1240 'lexer': '',
1236 1241 'op': 'mod',
1237 1242 'mode': c.file.mode
1238 1243 }
1239 1244 }
1240 1245
1241 ScmModel().update_nodes(
1246 commit = ScmModel().update_nodes(
1242 1247 user=self._rhodecode_db_user.user_id,
1243 1248 repo=self.db_repo,
1244 1249 message=message,
1245 1250 nodes=mapping,
1246 1251 parent_commit=c.commit,
1247 1252 )
1248 1253
1249 h.flash(
1250 _('Successfully committed changes to file `{}`').format(
1254 h.flash(_('Successfully committed changes to file `{}`').format(
1251 1255 h.escape(f_path)), category='success')
1256 default_redirect_url = h.route_path(
1257 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1258
1252 1259 except Exception:
1253 1260 log.exception('Error occurred during commit')
1254 1261 h.flash(_('Error occurred during commit'), category='error')
1255 raise HTTPFound(
1256 h.route_path('repo_commit', repo_name=self.db_repo_name,
1257 commit_id='tip'))
1262
1263 raise HTTPFound(default_redirect_url)
1258 1264
1259 1265 @LoginRequired()
1260 1266 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1261 1267 @view_config(
1262 1268 route_name='repo_files_add_file', request_method='GET',
1263 1269 renderer='rhodecode:templates/files/files_add.mako')
1264 1270 @view_config(
1265 1271 route_name='repo_files_upload_file', request_method='GET',
1266 1272 renderer='rhodecode:templates/files/files_upload.mako')
1267 1273 def repo_files_add_file(self):
1268 1274 _ = self.request.translate
1269 1275 c = self.load_default_context()
1270 1276 commit_id, f_path = self._get_commit_and_path()
1271 1277
1272 1278 self._ensure_not_locked()
1273 1279
1274 1280 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1275 1281 if c.commit is None:
1276 1282 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1277 c.default_message = (_('Added file via RhodeCode Enterprise'))
1278 c.f_path = f_path.lstrip('/') # ensure not relative path
1279 1283
1280 if self.rhodecode_vcs_repo.is_empty:
1284 if self.rhodecode_vcs_repo.is_empty():
1281 1285 # for empty repository we cannot check for current branch, we rely on
1282 1286 # c.commit.branch instead
1283 1287 _branch_name = c.commit.branch
1284 1288 is_head = True
1285 1289 else:
1286 1290 _branch_name, _sha_commit_id, is_head = \
1287 1291 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1288 1292
1289 if not is_head:
1290 h.flash(_('You can only add files with commit '
1291 'being a valid branch head.'), category='warning')
1292 raise HTTPFound(
1293 h.route_path('repo_files',
1294 repo_name=self.db_repo_name, commit_id='tip',
1295 f_path=f_path))
1293 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1294 self.check_branch_permission(_branch_name, commit_id=commit_id)
1296 1295
1297 self.check_branch_permission(_branch_name)
1296 c.default_message = (_('Added file via RhodeCode Enterprise'))
1297 c.f_path = f_path.lstrip('/') # ensure not relative path
1298 1298
1299 1299 return self._get_template_context(c)
1300 1300
1301 1301 @LoginRequired()
1302 1302 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1303 1303 @CSRFRequired()
1304 1304 @view_config(
1305 1305 route_name='repo_files_create_file', request_method='POST',
1306 1306 renderer=None)
1307 1307 def repo_files_create_file(self):
1308 1308 _ = self.request.translate
1309 1309 c = self.load_default_context()
1310 1310 commit_id, f_path = self._get_commit_and_path()
1311 1311
1312 1312 self._ensure_not_locked()
1313 1313
1314 r_post = self.request.POST
1315
1316 c.commit = self._get_commit_or_redirect(
1317 commit_id, redirect_after=False)
1314 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1318 1315 if c.commit is None:
1319 1316 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1320 1317
1321 if self.rhodecode_vcs_repo.is_empty:
1318 # calculate redirect URL
1319 if self.rhodecode_vcs_repo.is_empty():
1320 default_redirect_url = h.route_path(
1321 'repo_summary', repo_name=self.db_repo_name)
1322 else:
1323 default_redirect_url = h.route_path(
1324 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1325
1326 if self.rhodecode_vcs_repo.is_empty():
1322 1327 # for empty repository we cannot check for current branch, we rely on
1323 1328 # c.commit.branch instead
1324 1329 _branch_name = c.commit.branch
1325 1330 is_head = True
1326 1331 else:
1327 1332 _branch_name, _sha_commit_id, is_head = \
1328 1333 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1329 1334
1330 if not is_head:
1331 h.flash(_('You can only add files with commit '
1332 'being a valid branch head.'), category='warning')
1333 raise HTTPFound(
1334 h.route_path('repo_files',
1335 repo_name=self.db_repo_name, commit_id='tip',
1336 f_path=f_path))
1337
1338 self.check_branch_permission(_branch_name)
1335 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1336 self.check_branch_permission(_branch_name, commit_id=commit_id)
1339 1337
1340 1338 c.default_message = (_('Added file via RhodeCode Enterprise'))
1341 1339 c.f_path = f_path
1340
1341 r_post = self.request.POST
1342 message = r_post.get('message') or c.default_message
1343 filename = r_post.get('filename')
1342 1344 unix_mode = 0
1343 1345 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1344 1346
1345 message = r_post.get('message') or c.default_message
1346 filename = r_post.get('filename')
1347 location = r_post.get('location', '') # dir location
1348 file_obj = r_post.get('upload_file', None)
1349
1350 if file_obj is not None and hasattr(file_obj, 'filename'):
1351 filename = r_post.get('filename_upload')
1352 content = file_obj.file
1353
1354 if hasattr(content, 'file'):
1355 # non posix systems store real file under file attr
1356 content = content.file
1357
1358 if self.rhodecode_vcs_repo.is_empty:
1359 default_redirect_url = h.route_path(
1360 'repo_summary', repo_name=self.db_repo_name)
1361 else:
1362 default_redirect_url = h.route_path(
1363 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1364
1365 # If there's no commit, redirect to repo summary
1366 if type(c.commit) is EmptyCommit:
1367 redirect_url = h.route_path(
1368 'repo_summary', repo_name=self.db_repo_name)
1369 else:
1370 redirect_url = default_redirect_url
1371
1372 1347 if not filename:
1373 h.flash(_('No filename'), category='warning')
1348 # If there's no commit, redirect to repo summary
1349 if type(c.commit) is EmptyCommit:
1350 redirect_url = h.route_path(
1351 'repo_summary', repo_name=self.db_repo_name)
1352 else:
1353 redirect_url = default_redirect_url
1354 h.flash(_('No filename specified'), category='warning')
1374 1355 raise HTTPFound(redirect_url)
1375 1356
1376 # extract the location from filename,
1377 # allows using foo/bar.txt syntax to create subdirectories
1378 subdir_loc = filename.rsplit('/', 1)
1379 if len(subdir_loc) == 2:
1380 location = os.path.join(location, subdir_loc[0])
1357 root_path = f_path
1358 pure_path = self.create_pure_path(root_path, filename)
1359 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1381 1360
1382 # strip all crap out of file, just leave the basename
1383 filename = os.path.basename(filename)
1384 node_path = os.path.join(location, filename)
1385 1361 author = self._rhodecode_db_user.full_contact
1362 nodes = {
1363 node_path: {
1364 'content': content
1365 }
1366 }
1386 1367
1387 1368 try:
1388 nodes = {
1389 node_path: {
1390 'content': content
1391 }
1392 }
1393 ScmModel().create_nodes(
1369
1370 commit = ScmModel().create_nodes(
1394 1371 user=self._rhodecode_db_user.user_id,
1395 1372 repo=self.db_repo,
1396 1373 message=message,
1397 1374 nodes=nodes,
1398 1375 parent_commit=c.commit,
1399 1376 author=author,
1400 1377 )
1401 1378
1402 h.flash(
1403 _('Successfully committed new file `{}`').format(
1379 h.flash(_('Successfully committed new file `{}`').format(
1404 1380 h.escape(node_path)), category='success')
1381
1382 default_redirect_url = h.route_path(
1383 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1384
1405 1385 except NonRelativePathError:
1406 1386 log.exception('Non Relative path found')
1407 h.flash(_(
1408 'The location specified must be a relative path and must not '
1409 'contain .. in the path'), category='warning')
1387 h.flash(_('The location specified must be a relative path and must not '
1388 'contain .. in the path'), category='warning')
1410 1389 raise HTTPFound(default_redirect_url)
1411 1390 except (NodeError, NodeAlreadyExistsError) as e:
1412 1391 h.flash(_(h.escape(e)), category='error')
1413 1392 except Exception:
1414 1393 log.exception('Error occurred during commit')
1415 1394 h.flash(_('Error occurred during commit'), category='error')
1416 1395
1417 1396 raise HTTPFound(default_redirect_url)
1397
1398 @LoginRequired()
1399 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1400 @CSRFRequired()
1401 @view_config(
1402 route_name='repo_files_upload_file', request_method='POST',
1403 renderer='json_ext')
1404 def repo_files_upload_file(self):
1405 _ = self.request.translate
1406 c = self.load_default_context()
1407 commit_id, f_path = self._get_commit_and_path()
1408
1409 self._ensure_not_locked()
1410
1411 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1412 if c.commit is None:
1413 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1414
1415 # calculate redirect URL
1416 if self.rhodecode_vcs_repo.is_empty():
1417 default_redirect_url = h.route_path(
1418 'repo_summary', repo_name=self.db_repo_name)
1419 else:
1420 default_redirect_url = h.route_path(
1421 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1422
1423 if self.rhodecode_vcs_repo.is_empty():
1424 # for empty repository we cannot check for current branch, we rely on
1425 # c.commit.branch instead
1426 _branch_name = c.commit.branch
1427 is_head = True
1428 else:
1429 _branch_name, _sha_commit_id, is_head = \
1430 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1431
1432 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1433 if error:
1434 return {
1435 'error': error,
1436 'redirect_url': default_redirect_url
1437 }
1438 error = self.check_branch_permission(_branch_name, json_mode=True)
1439 if error:
1440 return {
1441 'error': error,
1442 'redirect_url': default_redirect_url
1443 }
1444
1445 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1446 c.f_path = f_path
1447
1448 r_post = self.request.POST
1449
1450 message = c.default_message
1451 user_message = r_post.getall('message')
1452 if isinstance(user_message, list) and user_message:
1453 # we take the first from duplicated results if it's not empty
1454 message = user_message[0] if user_message[0] else message
1455
1456 nodes = {}
1457
1458 for file_obj in r_post.getall('files_upload') or []:
1459 content = file_obj.file
1460 filename = file_obj.filename
1461
1462 root_path = f_path
1463 pure_path = self.create_pure_path(root_path, filename)
1464 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1465
1466 nodes[node_path] = {
1467 'content': content
1468 }
1469
1470 if not nodes:
1471 error = 'missing files'
1472 return {
1473 'error': error,
1474 'redirect_url': default_redirect_url
1475 }
1476
1477 author = self._rhodecode_db_user.full_contact
1478
1479 try:
1480 commit = ScmModel().create_nodes(
1481 user=self._rhodecode_db_user.user_id,
1482 repo=self.db_repo,
1483 message=message,
1484 nodes=nodes,
1485 parent_commit=c.commit,
1486 author=author,
1487 )
1488 if len(nodes) == 1:
1489 flash_message = _('Successfully committed {} new files').format(len(nodes))
1490 else:
1491 flash_message = _('Successfully committed 1 new file')
1492
1493 h.flash(flash_message, category='success')
1494
1495 default_redirect_url = h.route_path(
1496 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1497
1498 except NonRelativePathError:
1499 log.exception('Non Relative path found')
1500 error = _('The location specified must be a relative path and must not '
1501 'contain .. in the path')
1502 h.flash(error, category='warning')
1503
1504 return {
1505 'error': error,
1506 'redirect_url': default_redirect_url
1507 }
1508 except (NodeError, NodeAlreadyExistsError) as e:
1509 error = h.escape(e)
1510 h.flash(error, category='error')
1511
1512 return {
1513 'error': error,
1514 'redirect_url': default_redirect_url
1515 }
1516 except Exception:
1517 log.exception('Error occurred during commit')
1518 error = _('Error occurred during commit')
1519 h.flash(error, category='error')
1520 return {
1521 'error': error,
1522 'redirect_url': default_redirect_url
1523 }
1524
1525 return {
1526 'error': None,
1527 'redirect_url': default_redirect_url
1528 }
@@ -1,2075 +1,2078 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 return text[:size - len(postfix)] + postfix
157 157 return text
158 158
159 159
160 160 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 161 """
162 162 Reset button
163 163 """
164 164 _set_input_attrs(attrs, type, name, value)
165 165 _set_id_attr(attrs, id, name)
166 166 convert_boolean_attrs(attrs, ["disabled"])
167 167 return HTML.input(**attrs)
168 168
169 169 reset = _reset
170 170 safeid = _make_safe_id_component
171 171
172 172
173 173 def branding(name, length=40):
174 174 return truncate(name, length, indicator="")
175 175
176 176
177 177 def FID(raw_id, path):
178 178 """
179 179 Creates a unique ID for filenode based on it's hash of path and commit
180 180 it's safe to use in urls
181 181
182 182 :param raw_id:
183 183 :param path:
184 184 """
185 185
186 186 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 187
188 188
189 189 class _GetError(object):
190 190 """Get error from form_errors, and represent it as span wrapped error
191 191 message
192 192
193 193 :param field_name: field to fetch errors for
194 194 :param form_errors: form errors dict
195 195 """
196 196
197 197 def __call__(self, field_name, form_errors):
198 198 tmpl = """<span class="error_msg">%s</span>"""
199 199 if form_errors and field_name in form_errors:
200 200 return literal(tmpl % form_errors.get(field_name))
201 201
202 202
203 203 get_error = _GetError()
204 204
205 205
206 206 class _ToolTip(object):
207 207
208 208 def __call__(self, tooltip_title, trim_at=50):
209 209 """
210 210 Special function just to wrap our text into nice formatted
211 211 autowrapped text
212 212
213 213 :param tooltip_title:
214 214 """
215 215 tooltip_title = escape(tooltip_title)
216 216 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
217 217 return tooltip_title
218 218
219 219
220 220 tooltip = _ToolTip()
221 221
222 222 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
223 223
224 224
225 225 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
226 226 if isinstance(file_path, str):
227 227 file_path = safe_unicode(file_path)
228 228
229 229 route_qry = {'at': at_ref} if at_ref else None
230 230
231 231 # first segment is a `..` link to repo files
232 232 root_name = literal(u'<i class="icon-home"></i>')
233 233 url_segments = [
234 234 link_to(
235 235 root_name,
236 236 route_path(
237 237 'repo_files',
238 238 repo_name=repo_name,
239 239 commit_id=commit_id,
240 240 f_path='',
241 241 _query=route_qry),
242 242 )]
243 243
244 244 path_segments = file_path.split('/')
245 245 last_cnt = len(path_segments) - 1
246 246 for cnt, segment in enumerate(path_segments):
247 247 if not segment:
248 248 continue
249 249 segment_html = escape(segment)
250 250
251 251 last_item = cnt == last_cnt
252 252
253 253 if last_item and linkify_last_item is False:
254 254 # plain version
255 255 url_segments.append(segment_html)
256 256 else:
257 257 url_segments.append(
258 258 link_to(
259 259 segment_html,
260 260 route_path(
261 261 'repo_files',
262 262 repo_name=repo_name,
263 263 commit_id=commit_id,
264 264 f_path='/'.join(path_segments[:cnt + 1]),
265 265 _query=route_qry),
266 266 ))
267 267
268 268 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
269 269 if limit_items and len(limited_url_segments) < len(url_segments):
270 270 url_segments = limited_url_segments
271 271
272 272 full_path = file_path
273 273 icon = files_icon.format(escape(full_path))
274 274 if file_path == '':
275 275 return root_name
276 276 else:
277 277 return literal(' / '.join(url_segments) + icon)
278 278
279 279
280 280 def files_url_data(request):
281 281 matchdict = request.matchdict
282 282
283 283 if 'f_path' not in matchdict:
284 284 matchdict['f_path'] = ''
285 285
286 286 if 'commit_id' not in matchdict:
287 287 matchdict['commit_id'] = 'tip'
288 288
289 289 return json.dumps(matchdict)
290 290
291 291
292 292 def code_highlight(code, lexer, formatter, use_hl_filter=False):
293 293 """
294 294 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
295 295
296 296 If ``outfile`` is given and a valid file object (an object
297 297 with a ``write`` method), the result will be written to it, otherwise
298 298 it is returned as a string.
299 299 """
300 300 if use_hl_filter:
301 301 # add HL filter
302 302 from rhodecode.lib.index import search_utils
303 303 lexer.add_filter(search_utils.ElasticSearchHLFilter())
304 304 return pygments.format(pygments.lex(code, lexer), formatter)
305 305
306 306
307 307 class CodeHtmlFormatter(HtmlFormatter):
308 308 """
309 309 My code Html Formatter for source codes
310 310 """
311 311
312 312 def wrap(self, source, outfile):
313 313 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
314 314
315 315 def _wrap_code(self, source):
316 316 for cnt, it in enumerate(source):
317 317 i, t = it
318 318 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
319 319 yield i, t
320 320
321 321 def _wrap_tablelinenos(self, inner):
322 322 dummyoutfile = StringIO.StringIO()
323 323 lncount = 0
324 324 for t, line in inner:
325 325 if t:
326 326 lncount += 1
327 327 dummyoutfile.write(line)
328 328
329 329 fl = self.linenostart
330 330 mw = len(str(lncount + fl - 1))
331 331 sp = self.linenospecial
332 332 st = self.linenostep
333 333 la = self.lineanchors
334 334 aln = self.anchorlinenos
335 335 nocls = self.noclasses
336 336 if sp:
337 337 lines = []
338 338
339 339 for i in range(fl, fl + lncount):
340 340 if i % st == 0:
341 341 if i % sp == 0:
342 342 if aln:
343 343 lines.append('<a href="#%s%d" class="special">%*d</a>' %
344 344 (la, i, mw, i))
345 345 else:
346 346 lines.append('<span class="special">%*d</span>' % (mw, i))
347 347 else:
348 348 if aln:
349 349 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
350 350 else:
351 351 lines.append('%*d' % (mw, i))
352 352 else:
353 353 lines.append('')
354 354 ls = '\n'.join(lines)
355 355 else:
356 356 lines = []
357 357 for i in range(fl, fl + lncount):
358 358 if i % st == 0:
359 359 if aln:
360 360 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
361 361 else:
362 362 lines.append('%*d' % (mw, i))
363 363 else:
364 364 lines.append('')
365 365 ls = '\n'.join(lines)
366 366
367 367 # in case you wonder about the seemingly redundant <div> here: since the
368 368 # content in the other cell also is wrapped in a div, some browsers in
369 369 # some configurations seem to mess up the formatting...
370 370 if nocls:
371 371 yield 0, ('<table class="%stable">' % self.cssclass +
372 372 '<tr><td><div class="linenodiv" '
373 373 'style="background-color: #f0f0f0; padding-right: 10px">'
374 374 '<pre style="line-height: 125%">' +
375 375 ls + '</pre></div></td><td id="hlcode" class="code">')
376 376 else:
377 377 yield 0, ('<table class="%stable">' % self.cssclass +
378 378 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
379 379 ls + '</pre></div></td><td id="hlcode" class="code">')
380 380 yield 0, dummyoutfile.getvalue()
381 381 yield 0, '</td></tr></table>'
382 382
383 383
384 384 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
385 385 def __init__(self, **kw):
386 386 # only show these line numbers if set
387 387 self.only_lines = kw.pop('only_line_numbers', [])
388 388 self.query_terms = kw.pop('query_terms', [])
389 389 self.max_lines = kw.pop('max_lines', 5)
390 390 self.line_context = kw.pop('line_context', 3)
391 391 self.url = kw.pop('url', None)
392 392
393 393 super(CodeHtmlFormatter, self).__init__(**kw)
394 394
395 395 def _wrap_code(self, source):
396 396 for cnt, it in enumerate(source):
397 397 i, t = it
398 398 t = '<pre>%s</pre>' % t
399 399 yield i, t
400 400
401 401 def _wrap_tablelinenos(self, inner):
402 402 yield 0, '<table class="code-highlight %stable">' % self.cssclass
403 403
404 404 last_shown_line_number = 0
405 405 current_line_number = 1
406 406
407 407 for t, line in inner:
408 408 if not t:
409 409 yield t, line
410 410 continue
411 411
412 412 if current_line_number in self.only_lines:
413 413 if last_shown_line_number + 1 != current_line_number:
414 414 yield 0, '<tr>'
415 415 yield 0, '<td class="line">...</td>'
416 416 yield 0, '<td id="hlcode" class="code"></td>'
417 417 yield 0, '</tr>'
418 418
419 419 yield 0, '<tr>'
420 420 if self.url:
421 421 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
422 422 self.url, current_line_number, current_line_number)
423 423 else:
424 424 yield 0, '<td class="line"><a href="">%i</a></td>' % (
425 425 current_line_number)
426 426 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
427 427 yield 0, '</tr>'
428 428
429 429 last_shown_line_number = current_line_number
430 430
431 431 current_line_number += 1
432 432
433 433 yield 0, '</table>'
434 434
435 435
436 436 def hsv_to_rgb(h, s, v):
437 437 """ Convert hsv color values to rgb """
438 438
439 439 if s == 0.0:
440 440 return v, v, v
441 441 i = int(h * 6.0) # XXX assume int() truncates!
442 442 f = (h * 6.0) - i
443 443 p = v * (1.0 - s)
444 444 q = v * (1.0 - s * f)
445 445 t = v * (1.0 - s * (1.0 - f))
446 446 i = i % 6
447 447 if i == 0:
448 448 return v, t, p
449 449 if i == 1:
450 450 return q, v, p
451 451 if i == 2:
452 452 return p, v, t
453 453 if i == 3:
454 454 return p, q, v
455 455 if i == 4:
456 456 return t, p, v
457 457 if i == 5:
458 458 return v, p, q
459 459
460 460
461 461 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
462 462 """
463 463 Generator for getting n of evenly distributed colors using
464 464 hsv color and golden ratio. It always return same order of colors
465 465
466 466 :param n: number of colors to generate
467 467 :param saturation: saturation of returned colors
468 468 :param lightness: lightness of returned colors
469 469 :returns: RGB tuple
470 470 """
471 471
472 472 golden_ratio = 0.618033988749895
473 473 h = 0.22717784590367374
474 474
475 475 for _ in xrange(n):
476 476 h += golden_ratio
477 477 h %= 1
478 478 HSV_tuple = [h, saturation, lightness]
479 479 RGB_tuple = hsv_to_rgb(*HSV_tuple)
480 480 yield map(lambda x: str(int(x * 256)), RGB_tuple)
481 481
482 482
483 483 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
484 484 """
485 485 Returns a function which when called with an argument returns a unique
486 486 color for that argument, eg.
487 487
488 488 :param n: number of colors to generate
489 489 :param saturation: saturation of returned colors
490 490 :param lightness: lightness of returned colors
491 491 :returns: css RGB string
492 492
493 493 >>> color_hash = color_hasher()
494 494 >>> color_hash('hello')
495 495 'rgb(34, 12, 59)'
496 496 >>> color_hash('hello')
497 497 'rgb(34, 12, 59)'
498 498 >>> color_hash('other')
499 499 'rgb(90, 224, 159)'
500 500 """
501 501
502 502 color_dict = {}
503 503 cgenerator = unique_color_generator(
504 504 saturation=saturation, lightness=lightness)
505 505
506 506 def get_color_string(thing):
507 507 if thing in color_dict:
508 508 col = color_dict[thing]
509 509 else:
510 510 col = color_dict[thing] = cgenerator.next()
511 511 return "rgb(%s)" % (', '.join(col))
512 512
513 513 return get_color_string
514 514
515 515
516 516 def get_lexer_safe(mimetype=None, filepath=None):
517 517 """
518 518 Tries to return a relevant pygments lexer using mimetype/filepath name,
519 519 defaulting to plain text if none could be found
520 520 """
521 521 lexer = None
522 522 try:
523 523 if mimetype:
524 524 lexer = get_lexer_for_mimetype(mimetype)
525 525 if not lexer:
526 526 lexer = get_lexer_for_filename(filepath)
527 527 except pygments.util.ClassNotFound:
528 528 pass
529 529
530 530 if not lexer:
531 531 lexer = get_lexer_by_name('text')
532 532
533 533 return lexer
534 534
535 535
536 536 def get_lexer_for_filenode(filenode):
537 537 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
538 538 return lexer
539 539
540 540
541 541 def pygmentize(filenode, **kwargs):
542 542 """
543 543 pygmentize function using pygments
544 544
545 545 :param filenode:
546 546 """
547 547 lexer = get_lexer_for_filenode(filenode)
548 548 return literal(code_highlight(filenode.content, lexer,
549 549 CodeHtmlFormatter(**kwargs)))
550 550
551 551
552 552 def is_following_repo(repo_name, user_id):
553 553 from rhodecode.model.scm import ScmModel
554 554 return ScmModel().is_following_repo(repo_name, user_id)
555 555
556 556
557 557 class _Message(object):
558 558 """A message returned by ``Flash.pop_messages()``.
559 559
560 560 Converting the message to a string returns the message text. Instances
561 561 also have the following attributes:
562 562
563 563 * ``message``: the message text.
564 564 * ``category``: the category specified when the message was created.
565 565 """
566 566
567 567 def __init__(self, category, message):
568 568 self.category = category
569 569 self.message = message
570 570
571 571 def __str__(self):
572 572 return self.message
573 573
574 574 __unicode__ = __str__
575 575
576 576 def __html__(self):
577 577 return escape(safe_unicode(self.message))
578 578
579 579
580 580 class Flash(object):
581 581 # List of allowed categories. If None, allow any category.
582 582 categories = ["warning", "notice", "error", "success"]
583 583
584 584 # Default category if none is specified.
585 585 default_category = "notice"
586 586
587 587 def __init__(self, session_key="flash", categories=None,
588 588 default_category=None):
589 589 """
590 590 Instantiate a ``Flash`` object.
591 591
592 592 ``session_key`` is the key to save the messages under in the user's
593 593 session.
594 594
595 595 ``categories`` is an optional list which overrides the default list
596 596 of categories.
597 597
598 598 ``default_category`` overrides the default category used for messages
599 599 when none is specified.
600 600 """
601 601 self.session_key = session_key
602 602 if categories is not None:
603 603 self.categories = categories
604 604 if default_category is not None:
605 605 self.default_category = default_category
606 606 if self.categories and self.default_category not in self.categories:
607 607 raise ValueError(
608 608 "unrecognized default category %r" % (self.default_category,))
609 609
610 610 def pop_messages(self, session=None, request=None):
611 611 """
612 612 Return all accumulated messages and delete them from the session.
613 613
614 614 The return value is a list of ``Message`` objects.
615 615 """
616 616 messages = []
617 617
618 618 if not session:
619 619 if not request:
620 620 request = get_current_request()
621 621 session = request.session
622 622
623 623 # Pop the 'old' pylons flash messages. They are tuples of the form
624 624 # (category, message)
625 625 for cat, msg in session.pop(self.session_key, []):
626 626 messages.append(_Message(cat, msg))
627 627
628 628 # Pop the 'new' pyramid flash messages for each category as list
629 629 # of strings.
630 630 for cat in self.categories:
631 631 for msg in session.pop_flash(queue=cat):
632 632 messages.append(_Message(cat, msg))
633 633 # Map messages from the default queue to the 'notice' category.
634 634 for msg in session.pop_flash():
635 635 messages.append(_Message('notice', msg))
636 636
637 637 session.save()
638 638 return messages
639 639
640 640 def json_alerts(self, session=None, request=None):
641 641 payloads = []
642 642 messages = flash.pop_messages(session=session, request=request)
643 643 if messages:
644 644 for message in messages:
645 645 subdata = {}
646 646 if hasattr(message.message, 'rsplit'):
647 647 flash_data = message.message.rsplit('|DELIM|', 1)
648 648 org_message = flash_data[0]
649 649 if len(flash_data) > 1:
650 650 subdata = json.loads(flash_data[1])
651 651 else:
652 652 org_message = message.message
653 653 payloads.append({
654 654 'message': {
655 655 'message': u'{}'.format(org_message),
656 656 'level': message.category,
657 657 'force': True,
658 658 'subdata': subdata
659 659 }
660 660 })
661 661 return json.dumps(payloads)
662 662
663 663 def __call__(self, message, category=None, ignore_duplicate=False,
664 664 session=None, request=None):
665 665
666 666 if not session:
667 667 if not request:
668 668 request = get_current_request()
669 669 session = request.session
670 670
671 671 session.flash(
672 672 message, queue=category, allow_duplicate=not ignore_duplicate)
673 673
674 674
675 675 flash = Flash()
676 676
677 677 #==============================================================================
678 678 # SCM FILTERS available via h.
679 679 #==============================================================================
680 680 from rhodecode.lib.vcs.utils import author_name, author_email
681 681 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
682 682 from rhodecode.model.db import User, ChangesetStatus
683 683
684 684 capitalize = lambda x: x.capitalize()
685 685 email = author_email
686 686 short_id = lambda x: x[:12]
687 687 hide_credentials = lambda x: ''.join(credentials_filter(x))
688 688
689 689
690 690 import pytz
691 691 import tzlocal
692 692 local_timezone = tzlocal.get_localzone()
693 693
694 694
695 695 def age_component(datetime_iso, value=None, time_is_local=False):
696 696 title = value or format_date(datetime_iso)
697 697 tzinfo = '+00:00'
698 698
699 699 # detect if we have a timezone info, otherwise, add it
700 700 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
701 701 force_timezone = os.environ.get('RC_TIMEZONE', '')
702 702 if force_timezone:
703 703 force_timezone = pytz.timezone(force_timezone)
704 704 timezone = force_timezone or local_timezone
705 705 offset = timezone.localize(datetime_iso).strftime('%z')
706 706 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
707 707
708 708 return literal(
709 709 '<time class="timeago tooltip" '
710 710 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
711 711 datetime_iso, title, tzinfo))
712 712
713 713
714 714 def _shorten_commit_id(commit_id, commit_len=None):
715 715 if commit_len is None:
716 716 request = get_current_request()
717 717 commit_len = request.call_context.visual.show_sha_length
718 718 return commit_id[:commit_len]
719 719
720 720
721 721 def show_id(commit, show_idx=None, commit_len=None):
722 722 """
723 723 Configurable function that shows ID
724 724 by default it's r123:fffeeefffeee
725 725
726 726 :param commit: commit instance
727 727 """
728 728 if show_idx is None:
729 729 request = get_current_request()
730 730 show_idx = request.call_context.visual.show_revision_number
731 731
732 732 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
733 733 if show_idx:
734 734 return 'r%s:%s' % (commit.idx, raw_id)
735 735 else:
736 736 return '%s' % (raw_id, )
737 737
738 738
739 739 def format_date(date):
740 740 """
741 741 use a standardized formatting for dates used in RhodeCode
742 742
743 743 :param date: date/datetime object
744 744 :return: formatted date
745 745 """
746 746
747 747 if date:
748 748 _fmt = "%a, %d %b %Y %H:%M:%S"
749 749 return safe_unicode(date.strftime(_fmt))
750 750
751 751 return u""
752 752
753 753
754 754 class _RepoChecker(object):
755 755
756 756 def __init__(self, backend_alias):
757 757 self._backend_alias = backend_alias
758 758
759 759 def __call__(self, repository):
760 760 if hasattr(repository, 'alias'):
761 761 _type = repository.alias
762 762 elif hasattr(repository, 'repo_type'):
763 763 _type = repository.repo_type
764 764 else:
765 765 _type = repository
766 766 return _type == self._backend_alias
767 767
768 768
769 769 is_git = _RepoChecker('git')
770 770 is_hg = _RepoChecker('hg')
771 771 is_svn = _RepoChecker('svn')
772 772
773 773
774 774 def get_repo_type_by_name(repo_name):
775 775 repo = Repository.get_by_repo_name(repo_name)
776 776 if repo:
777 777 return repo.repo_type
778 778
779 779
780 780 def is_svn_without_proxy(repository):
781 781 if is_svn(repository):
782 782 from rhodecode.model.settings import VcsSettingsModel
783 783 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
784 784 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
785 785 return False
786 786
787 787
788 788 def discover_user(author):
789 789 """
790 790 Tries to discover RhodeCode User based on the autho string. Author string
791 791 is typically `FirstName LastName <email@address.com>`
792 792 """
793 793
794 794 # if author is already an instance use it for extraction
795 795 if isinstance(author, User):
796 796 return author
797 797
798 798 # Valid email in the attribute passed, see if they're in the system
799 799 _email = author_email(author)
800 800 if _email != '':
801 801 user = User.get_by_email(_email, case_insensitive=True, cache=True)
802 802 if user is not None:
803 803 return user
804 804
805 805 # Maybe it's a username, we try to extract it and fetch by username ?
806 806 _author = author_name(author)
807 807 user = User.get_by_username(_author, case_insensitive=True, cache=True)
808 808 if user is not None:
809 809 return user
810 810
811 811 return None
812 812
813 813
814 814 def email_or_none(author):
815 815 # extract email from the commit string
816 816 _email = author_email(author)
817 817
818 818 # If we have an email, use it, otherwise
819 819 # see if it contains a username we can get an email from
820 820 if _email != '':
821 821 return _email
822 822 else:
823 823 user = User.get_by_username(
824 824 author_name(author), case_insensitive=True, cache=True)
825 825
826 826 if user is not None:
827 827 return user.email
828 828
829 829 # No valid email, not a valid user in the system, none!
830 830 return None
831 831
832 832
833 833 def link_to_user(author, length=0, **kwargs):
834 834 user = discover_user(author)
835 835 # user can be None, but if we have it already it means we can re-use it
836 836 # in the person() function, so we save 1 intensive-query
837 837 if user:
838 838 author = user
839 839
840 840 display_person = person(author, 'username_or_name_or_email')
841 841 if length:
842 842 display_person = shorter(display_person, length)
843 843
844 844 if user:
845 845 return link_to(
846 846 escape(display_person),
847 847 route_path('user_profile', username=user.username),
848 848 **kwargs)
849 849 else:
850 850 return escape(display_person)
851 851
852 852
853 853 def link_to_group(users_group_name, **kwargs):
854 854 return link_to(
855 855 escape(users_group_name),
856 856 route_path('user_group_profile', user_group_name=users_group_name),
857 857 **kwargs)
858 858
859 859
860 860 def person(author, show_attr="username_and_name"):
861 861 user = discover_user(author)
862 862 if user:
863 863 return getattr(user, show_attr)
864 864 else:
865 865 _author = author_name(author)
866 866 _email = email(author)
867 867 return _author or _email
868 868
869 869
870 870 def author_string(email):
871 871 if email:
872 872 user = User.get_by_email(email, case_insensitive=True, cache=True)
873 873 if user:
874 874 if user.first_name or user.last_name:
875 875 return '%s %s &lt;%s&gt;' % (
876 876 user.first_name, user.last_name, email)
877 877 else:
878 878 return email
879 879 else:
880 880 return email
881 881 else:
882 882 return None
883 883
884 884
885 885 def person_by_id(id_, show_attr="username_and_name"):
886 886 # attr to return from fetched user
887 887 person_getter = lambda usr: getattr(usr, show_attr)
888 888
889 889 #maybe it's an ID ?
890 890 if str(id_).isdigit() or isinstance(id_, int):
891 891 id_ = int(id_)
892 892 user = User.get(id_)
893 893 if user is not None:
894 894 return person_getter(user)
895 895 return id_
896 896
897 897
898 898 def gravatar_with_user(request, author, show_disabled=False):
899 899 _render = request.get_partial_renderer(
900 900 'rhodecode:templates/base/base.mako')
901 901 return _render('gravatar_with_user', author, show_disabled=show_disabled)
902 902
903 903
904 904 tags_paterns = OrderedDict((
905 905 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
906 906 '<div class="metatag" tag="lang">\\2</div>')),
907 907
908 908 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
909 909 '<div class="metatag" tag="see">see: \\1 </div>')),
910 910
911 911 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
912 912 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
913 913
914 914 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
915 915 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
916 916
917 917 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
918 918 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
919 919
920 920 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
921 921 '<div class="metatag" tag="state \\1">\\1</div>')),
922 922
923 923 # label in grey
924 924 ('label', (re.compile(r'\[([a-z]+)\]'),
925 925 '<div class="metatag" tag="label">\\1</div>')),
926 926
927 927 # generic catch all in grey
928 928 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
929 929 '<div class="metatag" tag="generic">\\1</div>')),
930 930 ))
931 931
932 932
933 933 def extract_metatags(value):
934 934 """
935 935 Extract supported meta-tags from given text value
936 936 """
937 937 tags = []
938 938 if not value:
939 939 return tags, ''
940 940
941 941 for key, val in tags_paterns.items():
942 942 pat, replace_html = val
943 943 tags.extend([(key, x.group()) for x in pat.finditer(value)])
944 944 value = pat.sub('', value)
945 945
946 946 return tags, value
947 947
948 948
949 949 def style_metatag(tag_type, value):
950 950 """
951 951 converts tags from value into html equivalent
952 952 """
953 953 if not value:
954 954 return ''
955 955
956 956 html_value = value
957 957 tag_data = tags_paterns.get(tag_type)
958 958 if tag_data:
959 959 pat, replace_html = tag_data
960 960 # convert to plain `unicode` instead of a markup tag to be used in
961 961 # regex expressions. safe_unicode doesn't work here
962 962 html_value = pat.sub(replace_html, unicode(value))
963 963
964 964 return html_value
965 965
966 966
967 967 def bool2icon(value, show_at_false=True):
968 968 """
969 969 Returns boolean value of a given value, represented as html element with
970 970 classes that will represent icons
971 971
972 972 :param value: given value to convert to html node
973 973 """
974 974
975 975 if value: # does bool conversion
976 976 return HTML.tag('i', class_="icon-true")
977 977 else: # not true as bool
978 978 if show_at_false:
979 979 return HTML.tag('i', class_="icon-false")
980 980 return HTML.tag('i')
981 981
982 982 #==============================================================================
983 983 # PERMS
984 984 #==============================================================================
985 985 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
986 986 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
987 987 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
988 988 csrf_token_key
989 989
990 990
991 991 #==============================================================================
992 992 # GRAVATAR URL
993 993 #==============================================================================
994 994 class InitialsGravatar(object):
995 995 def __init__(self, email_address, first_name, last_name, size=30,
996 996 background=None, text_color='#fff'):
997 997 self.size = size
998 998 self.first_name = first_name
999 999 self.last_name = last_name
1000 1000 self.email_address = email_address
1001 1001 self.background = background or self.str2color(email_address)
1002 1002 self.text_color = text_color
1003 1003
1004 1004 def get_color_bank(self):
1005 1005 """
1006 1006 returns a predefined list of colors that gravatars can use.
1007 1007 Those are randomized distinct colors that guarantee readability and
1008 1008 uniqueness.
1009 1009
1010 1010 generated with: http://phrogz.net/css/distinct-colors.html
1011 1011 """
1012 1012 return [
1013 1013 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1014 1014 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1015 1015 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1016 1016 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1017 1017 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1018 1018 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1019 1019 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1020 1020 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1021 1021 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1022 1022 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1023 1023 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1024 1024 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1025 1025 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1026 1026 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1027 1027 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1028 1028 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1029 1029 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1030 1030 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1031 1031 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1032 1032 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1033 1033 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1034 1034 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1035 1035 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1036 1036 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1037 1037 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1038 1038 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1039 1039 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1040 1040 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1041 1041 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1042 1042 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1043 1043 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1044 1044 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1045 1045 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1046 1046 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1047 1047 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1048 1048 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1049 1049 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1050 1050 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1051 1051 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1052 1052 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1053 1053 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1054 1054 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1055 1055 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1056 1056 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1057 1057 '#4f8c46', '#368dd9', '#5c0073'
1058 1058 ]
1059 1059
1060 1060 def rgb_to_hex_color(self, rgb_tuple):
1061 1061 """
1062 1062 Converts an rgb_tuple passed to an hex color.
1063 1063
1064 1064 :param rgb_tuple: tuple with 3 ints represents rgb color space
1065 1065 """
1066 1066 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1067 1067
1068 1068 def email_to_int_list(self, email_str):
1069 1069 """
1070 1070 Get every byte of the hex digest value of email and turn it to integer.
1071 1071 It's going to be always between 0-255
1072 1072 """
1073 1073 digest = md5_safe(email_str.lower())
1074 1074 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1075 1075
1076 1076 def pick_color_bank_index(self, email_str, color_bank):
1077 1077 return self.email_to_int_list(email_str)[0] % len(color_bank)
1078 1078
1079 1079 def str2color(self, email_str):
1080 1080 """
1081 1081 Tries to map in a stable algorithm an email to color
1082 1082
1083 1083 :param email_str:
1084 1084 """
1085 1085 color_bank = self.get_color_bank()
1086 1086 # pick position (module it's length so we always find it in the
1087 1087 # bank even if it's smaller than 256 values
1088 1088 pos = self.pick_color_bank_index(email_str, color_bank)
1089 1089 return color_bank[pos]
1090 1090
1091 1091 def normalize_email(self, email_address):
1092 1092 import unicodedata
1093 1093 # default host used to fill in the fake/missing email
1094 1094 default_host = u'localhost'
1095 1095
1096 1096 if not email_address:
1097 1097 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1098 1098
1099 1099 email_address = safe_unicode(email_address)
1100 1100
1101 1101 if u'@' not in email_address:
1102 1102 email_address = u'%s@%s' % (email_address, default_host)
1103 1103
1104 1104 if email_address.endswith(u'@'):
1105 1105 email_address = u'%s%s' % (email_address, default_host)
1106 1106
1107 1107 email_address = unicodedata.normalize('NFKD', email_address)\
1108 1108 .encode('ascii', 'ignore')
1109 1109 return email_address
1110 1110
1111 1111 def get_initials(self):
1112 1112 """
1113 1113 Returns 2 letter initials calculated based on the input.
1114 1114 The algorithm picks first given email address, and takes first letter
1115 1115 of part before @, and then the first letter of server name. In case
1116 1116 the part before @ is in a format of `somestring.somestring2` it replaces
1117 1117 the server letter with first letter of somestring2
1118 1118
1119 1119 In case function was initialized with both first and lastname, this
1120 1120 overrides the extraction from email by first letter of the first and
1121 1121 last name. We add special logic to that functionality, In case Full name
1122 1122 is compound, like Guido Von Rossum, we use last part of the last name
1123 1123 (Von Rossum) picking `R`.
1124 1124
1125 1125 Function also normalizes the non-ascii characters to they ascii
1126 1126 representation, eg Δ„ => A
1127 1127 """
1128 1128 import unicodedata
1129 1129 # replace non-ascii to ascii
1130 1130 first_name = unicodedata.normalize(
1131 1131 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1132 1132 last_name = unicodedata.normalize(
1133 1133 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1134 1134
1135 1135 # do NFKD encoding, and also make sure email has proper format
1136 1136 email_address = self.normalize_email(self.email_address)
1137 1137
1138 1138 # first push the email initials
1139 1139 prefix, server = email_address.split('@', 1)
1140 1140
1141 1141 # check if prefix is maybe a 'first_name.last_name' syntax
1142 1142 _dot_split = prefix.rsplit('.', 1)
1143 1143 if len(_dot_split) == 2 and _dot_split[1]:
1144 1144 initials = [_dot_split[0][0], _dot_split[1][0]]
1145 1145 else:
1146 1146 initials = [prefix[0], server[0]]
1147 1147
1148 1148 # then try to replace either first_name or last_name
1149 1149 fn_letter = (first_name or " ")[0].strip()
1150 1150 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1151 1151
1152 1152 if fn_letter:
1153 1153 initials[0] = fn_letter
1154 1154
1155 1155 if ln_letter:
1156 1156 initials[1] = ln_letter
1157 1157
1158 1158 return ''.join(initials).upper()
1159 1159
1160 1160 def get_img_data_by_type(self, font_family, img_type):
1161 1161 default_user = """
1162 1162 <svg xmlns="http://www.w3.org/2000/svg"
1163 1163 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1164 1164 viewBox="-15 -10 439.165 429.164"
1165 1165
1166 1166 xml:space="preserve"
1167 1167 style="background:{background};" >
1168 1168
1169 1169 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1170 1170 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1171 1171 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1172 1172 168.596,153.916,216.671,
1173 1173 204.583,216.671z" fill="{text_color}"/>
1174 1174 <path d="M407.164,374.717L360.88,
1175 1175 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1176 1176 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1177 1177 15.366-44.203,23.488-69.076,23.488c-24.877,
1178 1178 0-48.762-8.122-69.078-23.488
1179 1179 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1180 1180 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1181 1181 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1182 1182 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1183 1183 19.402-10.527 C409.699,390.129,
1184 1184 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1185 1185 </svg>""".format(
1186 1186 size=self.size,
1187 1187 background='#979797', # @grey4
1188 1188 text_color=self.text_color,
1189 1189 font_family=font_family)
1190 1190
1191 1191 return {
1192 1192 "default_user": default_user
1193 1193 }[img_type]
1194 1194
1195 1195 def get_img_data(self, svg_type=None):
1196 1196 """
1197 1197 generates the svg metadata for image
1198 1198 """
1199 1199 fonts = [
1200 1200 '-apple-system',
1201 1201 'BlinkMacSystemFont',
1202 1202 'Segoe UI',
1203 1203 'Roboto',
1204 1204 'Oxygen-Sans',
1205 1205 'Ubuntu',
1206 1206 'Cantarell',
1207 1207 'Helvetica Neue',
1208 1208 'sans-serif'
1209 1209 ]
1210 1210 font_family = ','.join(fonts)
1211 1211 if svg_type:
1212 1212 return self.get_img_data_by_type(font_family, svg_type)
1213 1213
1214 1214 initials = self.get_initials()
1215 1215 img_data = """
1216 1216 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1217 1217 width="{size}" height="{size}"
1218 1218 style="width: 100%; height: 100%; background-color: {background}"
1219 1219 viewBox="0 0 {size} {size}">
1220 1220 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1221 1221 pointer-events="auto" fill="{text_color}"
1222 1222 font-family="{font_family}"
1223 1223 style="font-weight: 400; font-size: {f_size}px;">{text}
1224 1224 </text>
1225 1225 </svg>""".format(
1226 1226 size=self.size,
1227 1227 f_size=self.size/2.05, # scale the text inside the box nicely
1228 1228 background=self.background,
1229 1229 text_color=self.text_color,
1230 1230 text=initials.upper(),
1231 1231 font_family=font_family)
1232 1232
1233 1233 return img_data
1234 1234
1235 1235 def generate_svg(self, svg_type=None):
1236 1236 img_data = self.get_img_data(svg_type)
1237 1237 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1238 1238
1239 1239
1240 1240 def initials_gravatar(email_address, first_name, last_name, size=30):
1241 1241 svg_type = None
1242 1242 if email_address == User.DEFAULT_USER_EMAIL:
1243 1243 svg_type = 'default_user'
1244 1244 klass = InitialsGravatar(email_address, first_name, last_name, size)
1245 1245 return klass.generate_svg(svg_type=svg_type)
1246 1246
1247 1247
1248 1248 def gravatar_url(email_address, size=30, request=None):
1249 1249 request = get_current_request()
1250 1250 _use_gravatar = request.call_context.visual.use_gravatar
1251 1251 _gravatar_url = request.call_context.visual.gravatar_url
1252 1252
1253 1253 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1254 1254
1255 1255 email_address = email_address or User.DEFAULT_USER_EMAIL
1256 1256 if isinstance(email_address, unicode):
1257 1257 # hashlib crashes on unicode items
1258 1258 email_address = safe_str(email_address)
1259 1259
1260 1260 # empty email or default user
1261 1261 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1262 1262 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1263 1263
1264 1264 if _use_gravatar:
1265 1265 # TODO: Disuse pyramid thread locals. Think about another solution to
1266 1266 # get the host and schema here.
1267 1267 request = get_current_request()
1268 1268 tmpl = safe_str(_gravatar_url)
1269 1269 tmpl = tmpl.replace('{email}', email_address)\
1270 1270 .replace('{md5email}', md5_safe(email_address.lower())) \
1271 1271 .replace('{netloc}', request.host)\
1272 1272 .replace('{scheme}', request.scheme)\
1273 1273 .replace('{size}', safe_str(size))
1274 1274 return tmpl
1275 1275 else:
1276 1276 return initials_gravatar(email_address, '', '', size=size)
1277 1277
1278 1278
1279 1279 class Page(_Page):
1280 1280 """
1281 1281 Custom pager to match rendering style with paginator
1282 1282 """
1283 1283
1284 1284 def _get_pos(self, cur_page, max_page, items):
1285 1285 edge = (items / 2) + 1
1286 1286 if (cur_page <= edge):
1287 1287 radius = max(items / 2, items - cur_page)
1288 1288 elif (max_page - cur_page) < edge:
1289 1289 radius = (items - 1) - (max_page - cur_page)
1290 1290 else:
1291 1291 radius = items / 2
1292 1292
1293 1293 left = max(1, (cur_page - (radius)))
1294 1294 right = min(max_page, cur_page + (radius))
1295 1295 return left, cur_page, right
1296 1296
1297 1297 def _range(self, regexp_match):
1298 1298 """
1299 1299 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1300 1300
1301 1301 Arguments:
1302 1302
1303 1303 regexp_match
1304 1304 A "re" (regular expressions) match object containing the
1305 1305 radius of linked pages around the current page in
1306 1306 regexp_match.group(1) as a string
1307 1307
1308 1308 This function is supposed to be called as a callable in
1309 1309 re.sub.
1310 1310
1311 1311 """
1312 1312 radius = int(regexp_match.group(1))
1313 1313
1314 1314 # Compute the first and last page number within the radius
1315 1315 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1316 1316 # -> leftmost_page = 5
1317 1317 # -> rightmost_page = 9
1318 1318 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1319 1319 self.last_page,
1320 1320 (radius * 2) + 1)
1321 1321 nav_items = []
1322 1322
1323 1323 # Create a link to the first page (unless we are on the first page
1324 1324 # or there would be no need to insert '..' spacers)
1325 1325 if self.page != self.first_page and self.first_page < leftmost_page:
1326 1326 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1327 1327
1328 1328 # Insert dots if there are pages between the first page
1329 1329 # and the currently displayed page range
1330 1330 if leftmost_page - self.first_page > 1:
1331 1331 # Wrap in a SPAN tag if nolink_attr is set
1332 1332 text = '..'
1333 1333 if self.dotdot_attr:
1334 1334 text = HTML.span(c=text, **self.dotdot_attr)
1335 1335 nav_items.append(text)
1336 1336
1337 1337 for thispage in xrange(leftmost_page, rightmost_page + 1):
1338 1338 # Hilight the current page number and do not use a link
1339 1339 if thispage == self.page:
1340 1340 text = '%s' % (thispage,)
1341 1341 # Wrap in a SPAN tag if nolink_attr is set
1342 1342 if self.curpage_attr:
1343 1343 text = HTML.span(c=text, **self.curpage_attr)
1344 1344 nav_items.append(text)
1345 1345 # Otherwise create just a link to that page
1346 1346 else:
1347 1347 text = '%s' % (thispage,)
1348 1348 nav_items.append(self._pagerlink(thispage, text))
1349 1349
1350 1350 # Insert dots if there are pages between the displayed
1351 1351 # page numbers and the end of the page range
1352 1352 if self.last_page - rightmost_page > 1:
1353 1353 text = '..'
1354 1354 # Wrap in a SPAN tag if nolink_attr is set
1355 1355 if self.dotdot_attr:
1356 1356 text = HTML.span(c=text, **self.dotdot_attr)
1357 1357 nav_items.append(text)
1358 1358
1359 1359 # Create a link to the very last page (unless we are on the last
1360 1360 # page or there would be no need to insert '..' spacers)
1361 1361 if self.page != self.last_page and rightmost_page < self.last_page:
1362 1362 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1363 1363
1364 1364 ## prerender links
1365 1365 #_page_link = url.current()
1366 1366 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1367 1367 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1368 1368 return self.separator.join(nav_items)
1369 1369
1370 1370 def pager(self, format='~2~', page_param='page', partial_param='partial',
1371 1371 show_if_single_page=False, separator=' ', onclick=None,
1372 1372 symbol_first='<<', symbol_last='>>',
1373 1373 symbol_previous='<', symbol_next='>',
1374 1374 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1375 1375 curpage_attr={'class': 'pager_curpage'},
1376 1376 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1377 1377
1378 1378 self.curpage_attr = curpage_attr
1379 1379 self.separator = separator
1380 1380 self.pager_kwargs = kwargs
1381 1381 self.page_param = page_param
1382 1382 self.partial_param = partial_param
1383 1383 self.onclick = onclick
1384 1384 self.link_attr = link_attr
1385 1385 self.dotdot_attr = dotdot_attr
1386 1386
1387 1387 # Don't show navigator if there is no more than one page
1388 1388 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1389 1389 return ''
1390 1390
1391 1391 from string import Template
1392 1392 # Replace ~...~ in token format by range of pages
1393 1393 result = re.sub(r'~(\d+)~', self._range, format)
1394 1394
1395 1395 # Interpolate '%' variables
1396 1396 result = Template(result).safe_substitute({
1397 1397 'first_page': self.first_page,
1398 1398 'last_page': self.last_page,
1399 1399 'page': self.page,
1400 1400 'page_count': self.page_count,
1401 1401 'items_per_page': self.items_per_page,
1402 1402 'first_item': self.first_item,
1403 1403 'last_item': self.last_item,
1404 1404 'item_count': self.item_count,
1405 1405 'link_first': self.page > self.first_page and \
1406 1406 self._pagerlink(self.first_page, symbol_first) or '',
1407 1407 'link_last': self.page < self.last_page and \
1408 1408 self._pagerlink(self.last_page, symbol_last) or '',
1409 1409 'link_previous': self.previous_page and \
1410 1410 self._pagerlink(self.previous_page, symbol_previous) \
1411 1411 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1412 1412 'link_next': self.next_page and \
1413 1413 self._pagerlink(self.next_page, symbol_next) \
1414 1414 or HTML.span(symbol_next, class_="pg-next disabled")
1415 1415 })
1416 1416
1417 1417 return literal(result)
1418 1418
1419 1419
1420 1420 #==============================================================================
1421 1421 # REPO PAGER, PAGER FOR REPOSITORY
1422 1422 #==============================================================================
1423 1423 class RepoPage(Page):
1424 1424
1425 1425 def __init__(self, collection, page=1, items_per_page=20,
1426 1426 item_count=None, url=None, **kwargs):
1427 1427
1428 1428 """Create a "RepoPage" instance. special pager for paging
1429 1429 repository
1430 1430 """
1431 1431 self._url_generator = url
1432 1432
1433 1433 # Safe the kwargs class-wide so they can be used in the pager() method
1434 1434 self.kwargs = kwargs
1435 1435
1436 1436 # Save a reference to the collection
1437 1437 self.original_collection = collection
1438 1438
1439 1439 self.collection = collection
1440 1440
1441 1441 # The self.page is the number of the current page.
1442 1442 # The first page has the number 1!
1443 1443 try:
1444 1444 self.page = int(page) # make it int() if we get it as a string
1445 1445 except (ValueError, TypeError):
1446 1446 self.page = 1
1447 1447
1448 1448 self.items_per_page = items_per_page
1449 1449
1450 1450 # Unless the user tells us how many items the collections has
1451 1451 # we calculate that ourselves.
1452 1452 if item_count is not None:
1453 1453 self.item_count = item_count
1454 1454 else:
1455 1455 self.item_count = len(self.collection)
1456 1456
1457 1457 # Compute the number of the first and last available page
1458 1458 if self.item_count > 0:
1459 1459 self.first_page = 1
1460 1460 self.page_count = int(math.ceil(float(self.item_count) /
1461 1461 self.items_per_page))
1462 1462 self.last_page = self.first_page + self.page_count - 1
1463 1463
1464 1464 # Make sure that the requested page number is the range of
1465 1465 # valid pages
1466 1466 if self.page > self.last_page:
1467 1467 self.page = self.last_page
1468 1468 elif self.page < self.first_page:
1469 1469 self.page = self.first_page
1470 1470
1471 1471 # Note: the number of items on this page can be less than
1472 1472 # items_per_page if the last page is not full
1473 1473 self.first_item = max(0, (self.item_count) - (self.page *
1474 1474 items_per_page))
1475 1475 self.last_item = ((self.item_count - 1) - items_per_page *
1476 1476 (self.page - 1))
1477 1477
1478 1478 self.items = list(self.collection[self.first_item:self.last_item + 1])
1479 1479
1480 1480 # Links to previous and next page
1481 1481 if self.page > self.first_page:
1482 1482 self.previous_page = self.page - 1
1483 1483 else:
1484 1484 self.previous_page = None
1485 1485
1486 1486 if self.page < self.last_page:
1487 1487 self.next_page = self.page + 1
1488 1488 else:
1489 1489 self.next_page = None
1490 1490
1491 1491 # No items available
1492 1492 else:
1493 1493 self.first_page = None
1494 1494 self.page_count = 0
1495 1495 self.last_page = None
1496 1496 self.first_item = None
1497 1497 self.last_item = None
1498 1498 self.previous_page = None
1499 1499 self.next_page = None
1500 1500 self.items = []
1501 1501
1502 1502 # This is a subclass of the 'list' type. Initialise the list now.
1503 1503 list.__init__(self, reversed(self.items))
1504 1504
1505 1505
1506 1506 def breadcrumb_repo_link(repo):
1507 1507 """
1508 1508 Makes a breadcrumbs path link to repo
1509 1509
1510 1510 ex::
1511 1511 group >> subgroup >> repo
1512 1512
1513 1513 :param repo: a Repository instance
1514 1514 """
1515 1515
1516 1516 path = [
1517 1517 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1518 1518 title='last change:{}'.format(format_date(group.last_commit_change)))
1519 1519 for group in repo.groups_with_parents
1520 1520 ] + [
1521 1521 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1522 1522 title='last change:{}'.format(format_date(repo.last_commit_change)))
1523 1523 ]
1524 1524
1525 1525 return literal(' &raquo; '.join(path))
1526 1526
1527 1527
1528 1528 def breadcrumb_repo_group_link(repo_group):
1529 1529 """
1530 1530 Makes a breadcrumbs path link to repo
1531 1531
1532 1532 ex::
1533 1533 group >> subgroup
1534 1534
1535 1535 :param repo_group: a Repository Group instance
1536 1536 """
1537 1537
1538 1538 path = [
1539 1539 link_to(group.name,
1540 1540 route_path('repo_group_home', repo_group_name=group.group_name),
1541 1541 title='last change:{}'.format(format_date(group.last_commit_change)))
1542 1542 for group in repo_group.parents
1543 1543 ] + [
1544 1544 link_to(repo_group.name,
1545 1545 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1546 1546 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1547 1547 ]
1548 1548
1549 1549 return literal(' &raquo; '.join(path))
1550 1550
1551 1551
1552 1552 def format_byte_size_binary(file_size):
1553 1553 """
1554 1554 Formats file/folder sizes to standard.
1555 1555 """
1556 1556 if file_size is None:
1557 1557 file_size = 0
1558 1558
1559 1559 formatted_size = format_byte_size(file_size, binary=True)
1560 1560 return formatted_size
1561 1561
1562 1562
1563 1563 def urlify_text(text_, safe=True):
1564 1564 """
1565 1565 Extrac urls from text and make html links out of them
1566 1566
1567 1567 :param text_:
1568 1568 """
1569 1569
1570 1570 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1571 1571 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1572 1572
1573 1573 def url_func(match_obj):
1574 1574 url_full = match_obj.groups()[0]
1575 1575 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1576 1576 _newtext = url_pat.sub(url_func, text_)
1577 1577 if safe:
1578 1578 return literal(_newtext)
1579 1579 return _newtext
1580 1580
1581 1581
1582 1582 def urlify_commits(text_, repository):
1583 1583 """
1584 1584 Extract commit ids from text and make link from them
1585 1585
1586 1586 :param text_:
1587 1587 :param repository: repo name to build the URL with
1588 1588 """
1589 1589
1590 1590 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1591 1591
1592 1592 def url_func(match_obj):
1593 1593 commit_id = match_obj.groups()[1]
1594 1594 pref = match_obj.groups()[0]
1595 1595 suf = match_obj.groups()[2]
1596 1596
1597 1597 tmpl = (
1598 1598 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1599 1599 '%(commit_id)s</a>%(suf)s'
1600 1600 )
1601 1601 return tmpl % {
1602 1602 'pref': pref,
1603 1603 'cls': 'revision-link',
1604 1604 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1605 1605 'commit_id': commit_id,
1606 1606 'suf': suf
1607 1607 }
1608 1608
1609 1609 newtext = URL_PAT.sub(url_func, text_)
1610 1610
1611 1611 return newtext
1612 1612
1613 1613
1614 1614 def _process_url_func(match_obj, repo_name, uid, entry,
1615 1615 return_raw_data=False, link_format='html'):
1616 1616 pref = ''
1617 1617 if match_obj.group().startswith(' '):
1618 1618 pref = ' '
1619 1619
1620 1620 issue_id = ''.join(match_obj.groups())
1621 1621
1622 1622 if link_format == 'html':
1623 1623 tmpl = (
1624 1624 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1625 1625 '%(issue-prefix)s%(id-repr)s'
1626 1626 '</a>')
1627 1627 elif link_format == 'rst':
1628 1628 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1629 1629 elif link_format == 'markdown':
1630 1630 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1631 1631 else:
1632 1632 raise ValueError('Bad link_format:{}'.format(link_format))
1633 1633
1634 1634 (repo_name_cleaned,
1635 1635 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1636 1636
1637 1637 # variables replacement
1638 1638 named_vars = {
1639 1639 'id': issue_id,
1640 1640 'repo': repo_name,
1641 1641 'repo_name': repo_name_cleaned,
1642 1642 'group_name': parent_group_name
1643 1643 }
1644 1644 # named regex variables
1645 1645 named_vars.update(match_obj.groupdict())
1646 1646 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1647 1647
1648 1648 def quote_cleaner(input_str):
1649 1649 """Remove quotes as it's HTML"""
1650 1650 return input_str.replace('"', '')
1651 1651
1652 1652 data = {
1653 1653 'pref': pref,
1654 1654 'cls': quote_cleaner('issue-tracker-link'),
1655 1655 'url': quote_cleaner(_url),
1656 1656 'id-repr': issue_id,
1657 1657 'issue-prefix': entry['pref'],
1658 1658 'serv': entry['url'],
1659 1659 }
1660 1660 if return_raw_data:
1661 1661 return {
1662 1662 'id': issue_id,
1663 1663 'url': _url
1664 1664 }
1665 1665 return tmpl % data
1666 1666
1667 1667
1668 1668 def get_active_pattern_entries(repo_name):
1669 1669 repo = None
1670 1670 if repo_name:
1671 1671 # Retrieving repo_name to avoid invalid repo_name to explode on
1672 1672 # IssueTrackerSettingsModel but still passing invalid name further down
1673 1673 repo = Repository.get_by_repo_name(repo_name, cache=True)
1674 1674
1675 1675 settings_model = IssueTrackerSettingsModel(repo=repo)
1676 1676 active_entries = settings_model.get_settings(cache=True)
1677 1677 return active_entries
1678 1678
1679 1679
1680 1680 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1681 1681
1682 1682 allowed_formats = ['html', 'rst', 'markdown']
1683 1683 if link_format not in allowed_formats:
1684 1684 raise ValueError('Link format can be only one of:{} got {}'.format(
1685 1685 allowed_formats, link_format))
1686 1686
1687 1687 active_entries = active_entries or get_active_pattern_entries(repo_name)
1688 1688 issues_data = []
1689 1689 newtext = text_string
1690 1690
1691 1691 for uid, entry in active_entries.items():
1692 1692 log.debug('found issue tracker entry with uid %s', uid)
1693 1693
1694 1694 if not (entry['pat'] and entry['url']):
1695 1695 log.debug('skipping due to missing data')
1696 1696 continue
1697 1697
1698 1698 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1699 1699 uid, entry['pat'], entry['url'], entry['pref'])
1700 1700
1701 1701 try:
1702 1702 pattern = re.compile(r'%s' % entry['pat'])
1703 1703 except re.error:
1704 1704 log.exception(
1705 1705 'issue tracker pattern: `%s` failed to compile',
1706 1706 entry['pat'])
1707 1707 continue
1708 1708
1709 1709 data_func = partial(
1710 1710 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1711 1711 return_raw_data=True)
1712 1712
1713 1713 for match_obj in pattern.finditer(text_string):
1714 1714 issues_data.append(data_func(match_obj))
1715 1715
1716 1716 url_func = partial(
1717 1717 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1718 1718 link_format=link_format)
1719 1719
1720 1720 newtext = pattern.sub(url_func, newtext)
1721 1721 log.debug('processed prefix:uid `%s`', uid)
1722 1722
1723 1723 return newtext, issues_data
1724 1724
1725 1725
1726 1726 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1727 1727 """
1728 1728 Parses given text message and makes proper links.
1729 1729 issues are linked to given issue-server, and rest is a commit link
1730 1730
1731 1731 :param commit_text:
1732 1732 :param repository:
1733 1733 """
1734 1734 def escaper(string):
1735 1735 return string.replace('<', '&lt;').replace('>', '&gt;')
1736 1736
1737 1737 newtext = escaper(commit_text)
1738 1738
1739 1739 # extract http/https links and make them real urls
1740 1740 newtext = urlify_text(newtext, safe=False)
1741 1741
1742 1742 # urlify commits - extract commit ids and make link out of them, if we have
1743 1743 # the scope of repository present.
1744 1744 if repository:
1745 1745 newtext = urlify_commits(newtext, repository)
1746 1746
1747 1747 # process issue tracker patterns
1748 1748 newtext, issues = process_patterns(newtext, repository or '',
1749 1749 active_entries=active_pattern_entries)
1750 1750
1751 1751 return literal(newtext)
1752 1752
1753 1753
1754 1754 def render_binary(repo_name, file_obj):
1755 1755 """
1756 1756 Choose how to render a binary file
1757 1757 """
1758 1758
1759 1759 filename = file_obj.name
1760 1760
1761 1761 # images
1762 1762 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1763 1763 if fnmatch.fnmatch(filename, pat=ext):
1764 1764 alt = escape(filename)
1765 1765 src = route_path(
1766 1766 'repo_file_raw', repo_name=repo_name,
1767 1767 commit_id=file_obj.commit.raw_id,
1768 1768 f_path=file_obj.path)
1769 1769 return literal(
1770 1770 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1771 1771
1772 1772
1773 1773 def renderer_from_filename(filename, exclude=None):
1774 1774 """
1775 1775 choose a renderer based on filename, this works only for text based files
1776 1776 """
1777 1777
1778 1778 # ipython
1779 1779 for ext in ['*.ipynb']:
1780 1780 if fnmatch.fnmatch(filename, pat=ext):
1781 1781 return 'jupyter'
1782 1782
1783 1783 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1784 1784 if is_markup:
1785 1785 return is_markup
1786 1786 return None
1787 1787
1788 1788
1789 1789 def render(source, renderer='rst', mentions=False, relative_urls=None,
1790 1790 repo_name=None):
1791 1791
1792 1792 def maybe_convert_relative_links(html_source):
1793 1793 if relative_urls:
1794 1794 return relative_links(html_source, relative_urls)
1795 1795 return html_source
1796 1796
1797 1797 if renderer == 'plain':
1798 1798 return literal(
1799 1799 MarkupRenderer.plain(source, leading_newline=False))
1800 1800
1801 1801 elif renderer == 'rst':
1802 1802 if repo_name:
1803 1803 # process patterns on comments if we pass in repo name
1804 1804 source, issues = process_patterns(
1805 1805 source, repo_name, link_format='rst')
1806 1806
1807 1807 return literal(
1808 1808 '<div class="rst-block">%s</div>' %
1809 1809 maybe_convert_relative_links(
1810 1810 MarkupRenderer.rst(source, mentions=mentions)))
1811 1811
1812 1812 elif renderer == 'markdown':
1813 1813 if repo_name:
1814 1814 # process patterns on comments if we pass in repo name
1815 1815 source, issues = process_patterns(
1816 1816 source, repo_name, link_format='markdown')
1817 1817
1818 1818 return literal(
1819 1819 '<div class="markdown-block">%s</div>' %
1820 1820 maybe_convert_relative_links(
1821 1821 MarkupRenderer.markdown(source, flavored=True,
1822 1822 mentions=mentions)))
1823 1823
1824 1824 elif renderer == 'jupyter':
1825 1825 return literal(
1826 1826 '<div class="ipynb">%s</div>' %
1827 1827 maybe_convert_relative_links(
1828 1828 MarkupRenderer.jupyter(source)))
1829 1829
1830 1830 # None means just show the file-source
1831 1831 return None
1832 1832
1833 1833
1834 1834 def commit_status(repo, commit_id):
1835 1835 return ChangesetStatusModel().get_status(repo, commit_id)
1836 1836
1837 1837
1838 1838 def commit_status_lbl(commit_status):
1839 1839 return dict(ChangesetStatus.STATUSES).get(commit_status)
1840 1840
1841 1841
1842 1842 def commit_time(repo_name, commit_id):
1843 1843 repo = Repository.get_by_repo_name(repo_name)
1844 1844 commit = repo.get_commit(commit_id=commit_id)
1845 1845 return commit.date
1846 1846
1847 1847
1848 1848 def get_permission_name(key):
1849 1849 return dict(Permission.PERMS).get(key)
1850 1850
1851 1851
1852 1852 def journal_filter_help(request):
1853 1853 _ = request.translate
1854 1854 from rhodecode.lib.audit_logger import ACTIONS
1855 1855 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1856 1856
1857 1857 return _(
1858 1858 'Example filter terms:\n' +
1859 1859 ' repository:vcs\n' +
1860 1860 ' username:marcin\n' +
1861 1861 ' username:(NOT marcin)\n' +
1862 1862 ' action:*push*\n' +
1863 1863 ' ip:127.0.0.1\n' +
1864 1864 ' date:20120101\n' +
1865 1865 ' date:[20120101100000 TO 20120102]\n' +
1866 1866 '\n' +
1867 1867 'Actions: {actions}\n' +
1868 1868 '\n' +
1869 1869 'Generate wildcards using \'*\' character:\n' +
1870 1870 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1871 1871 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1872 1872 '\n' +
1873 1873 'Optional AND / OR operators in queries\n' +
1874 1874 ' "repository:vcs OR repository:test"\n' +
1875 1875 ' "username:test AND repository:test*"\n'
1876 1876 ).format(actions=actions)
1877 1877
1878 1878
1879 1879 def not_mapped_error(repo_name):
1880 1880 from rhodecode.translation import _
1881 1881 flash(_('%s repository is not mapped to db perhaps'
1882 1882 ' it was created or renamed from the filesystem'
1883 1883 ' please run the application again'
1884 1884 ' in order to rescan repositories') % repo_name, category='error')
1885 1885
1886 1886
1887 1887 def ip_range(ip_addr):
1888 1888 from rhodecode.model.db import UserIpMap
1889 1889 s, e = UserIpMap._get_ip_range(ip_addr)
1890 1890 return '%s - %s' % (s, e)
1891 1891
1892 1892
1893 1893 def form(url, method='post', needs_csrf_token=True, **attrs):
1894 1894 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1895 1895 if method.lower() != 'get' and needs_csrf_token:
1896 1896 raise Exception(
1897 1897 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1898 1898 'CSRF token. If the endpoint does not require such token you can ' +
1899 1899 'explicitly set the parameter needs_csrf_token to false.')
1900 1900
1901 1901 return wh_form(url, method=method, **attrs)
1902 1902
1903 1903
1904 1904 def secure_form(form_url, method="POST", multipart=False, **attrs):
1905 1905 """Start a form tag that points the action to an url. This
1906 1906 form tag will also include the hidden field containing
1907 1907 the auth token.
1908 1908
1909 1909 The url options should be given either as a string, or as a
1910 1910 ``url()`` function. The method for the form defaults to POST.
1911 1911
1912 1912 Options:
1913 1913
1914 1914 ``multipart``
1915 1915 If set to True, the enctype is set to "multipart/form-data".
1916 1916 ``method``
1917 1917 The method to use when submitting the form, usually either
1918 1918 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1919 1919 hidden input with name _method is added to simulate the verb
1920 1920 over POST.
1921 1921
1922 1922 """
1923 1923 from webhelpers.pylonslib.secure_form import insecure_form
1924 1924
1925 1925 if 'request' in attrs:
1926 1926 session = attrs['request'].session
1927 1927 del attrs['request']
1928 1928 else:
1929 1929 raise ValueError(
1930 1930 'Calling this form requires request= to be passed as argument')
1931 1931
1932 1932 form = insecure_form(form_url, method, multipart, **attrs)
1933 1933 token = literal(
1934 1934 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1935 1935 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1936 1936
1937 1937 return literal("%s\n%s" % (form, token))
1938 1938
1939 1939
1940 1940 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1941 1941 select_html = select(name, selected, options, **attrs)
1942
1942 1943 select2 = """
1943 1944 <script>
1944 1945 $(document).ready(function() {
1945 1946 $('#%s').select2({
1946 containerCssClass: 'drop-menu',
1947 containerCssClass: 'drop-menu %s',
1947 1948 dropdownCssClass: 'drop-menu-dropdown',
1948 1949 dropdownAutoWidth: true%s
1949 1950 });
1950 1951 });
1951 1952 </script>
1952 1953 """
1954
1953 1955 filter_option = """,
1954 1956 minimumResultsForSearch: -1
1955 1957 """
1956 1958 input_id = attrs.get('id') or name
1959 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1957 1960 filter_enabled = "" if enable_filter else filter_option
1958 select_script = literal(select2 % (input_id, filter_enabled))
1961 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1959 1962
1960 1963 return literal(select_html+select_script)
1961 1964
1962 1965
1963 1966 def get_visual_attr(tmpl_context_var, attr_name):
1964 1967 """
1965 1968 A safe way to get a variable from visual variable of template context
1966 1969
1967 1970 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1968 1971 :param attr_name: name of the attribute we fetch from the c.visual
1969 1972 """
1970 1973 visual = getattr(tmpl_context_var, 'visual', None)
1971 1974 if not visual:
1972 1975 return
1973 1976 else:
1974 1977 return getattr(visual, attr_name, None)
1975 1978
1976 1979
1977 1980 def get_last_path_part(file_node):
1978 1981 if not file_node.path:
1979 1982 return u'/'
1980 1983
1981 1984 path = safe_unicode(file_node.path.split('/')[-1])
1982 1985 return u'../' + path
1983 1986
1984 1987
1985 1988 def route_url(*args, **kwargs):
1986 1989 """
1987 1990 Wrapper around pyramids `route_url` (fully qualified url) function.
1988 1991 """
1989 1992 req = get_current_request()
1990 1993 return req.route_url(*args, **kwargs)
1991 1994
1992 1995
1993 1996 def route_path(*args, **kwargs):
1994 1997 """
1995 1998 Wrapper around pyramids `route_path` function.
1996 1999 """
1997 2000 req = get_current_request()
1998 2001 return req.route_path(*args, **kwargs)
1999 2002
2000 2003
2001 2004 def route_path_or_none(*args, **kwargs):
2002 2005 try:
2003 2006 return route_path(*args, **kwargs)
2004 2007 except KeyError:
2005 2008 return None
2006 2009
2007 2010
2008 2011 def current_route_path(request, **kw):
2009 2012 new_args = request.GET.mixed()
2010 2013 new_args.update(kw)
2011 2014 return request.current_route_path(_query=new_args)
2012 2015
2013 2016
2014 2017 def api_call_example(method, args):
2015 2018 """
2016 2019 Generates an API call example via CURL
2017 2020 """
2018 2021 args_json = json.dumps(OrderedDict([
2019 2022 ('id', 1),
2020 2023 ('auth_token', 'SECRET'),
2021 2024 ('method', method),
2022 2025 ('args', args)
2023 2026 ]))
2024 2027 return literal(
2025 2028 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2026 2029 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2027 2030 "and needs to be of `api calls` role."
2028 2031 .format(
2029 2032 api_url=route_url('apiv2'),
2030 2033 token_url=route_url('my_account_auth_tokens'),
2031 2034 data=args_json))
2032 2035
2033 2036
2034 2037 def notification_description(notification, request):
2035 2038 """
2036 2039 Generate notification human readable description based on notification type
2037 2040 """
2038 2041 from rhodecode.model.notification import NotificationModel
2039 2042 return NotificationModel().make_description(
2040 2043 notification, translate=request.translate)
2041 2044
2042 2045
2043 2046 def go_import_header(request, db_repo=None):
2044 2047 """
2045 2048 Creates a header for go-import functionality in Go Lang
2046 2049 """
2047 2050
2048 2051 if not db_repo:
2049 2052 return
2050 2053 if 'go-get' not in request.GET:
2051 2054 return
2052 2055
2053 2056 clone_url = db_repo.clone_url()
2054 2057 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2055 2058 # we have a repo and go-get flag,
2056 2059 return literal('<meta name="go-import" content="{} {} {}">'.format(
2057 2060 prefix, db_repo.repo_type, clone_url))
2058 2061
2059 2062
2060 2063 def reviewer_as_json(*args, **kwargs):
2061 2064 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2062 2065 return _reviewer_as_json(*args, **kwargs)
2063 2066
2064 2067
2065 2068 def get_repo_view_type(request):
2066 2069 route_name = request.matched_route.name
2067 2070 route_to_view_type = {
2068 2071 'repo_changelog': 'commits',
2069 2072 'repo_commits': 'commits',
2070 2073 'repo_files': 'files',
2071 2074 'repo_summary': 'summary',
2072 2075 'repo_commit': 'commit'
2073 2076 }
2074 2077
2075 2078 return route_to_view_type.get(route_name)
@@ -1,939 +1,941 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Scm model for RhodeCode
23 23 """
24 24
25 25 import os.path
26 26 import traceback
27 27 import logging
28 28 import cStringIO
29 29
30 30 from sqlalchemy import func
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 import rhodecode
34 34 from rhodecode.lib.vcs import get_backend
35 35 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib import helpers as h, rc_cache
39 39 from rhodecode.lib.auth import (
40 40 HasRepoPermissionAny, HasRepoGroupPermissionAny,
41 41 HasUserGroupPermissionAny)
42 42 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
43 43 from rhodecode.lib import hooks_utils
44 44 from rhodecode.lib.utils import (
45 45 get_filesystem_repos, make_db_config)
46 46 from rhodecode.lib.utils2 import (safe_str, safe_unicode)
47 47 from rhodecode.lib.system_info import get_system_info
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.db import (
50 50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 51 PullRequest)
52 52 from rhodecode.model.settings import VcsSettingsModel
53 53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UserTemp(object):
59 59 def __init__(self, user_id):
60 60 self.user_id = user_id
61 61
62 62 def __repr__(self):
63 63 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64 64
65 65
66 66 class RepoTemp(object):
67 67 def __init__(self, repo_id):
68 68 self.repo_id = repo_id
69 69
70 70 def __repr__(self):
71 71 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72 72
73 73
74 74 class SimpleCachedRepoList(object):
75 75 """
76 76 Lighter version of of iteration of repos without the scm initialisation,
77 77 and with cache usage
78 78 """
79 79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 80 self.db_repo_list = db_repo_list
81 81 self.repos_path = repos_path
82 82 self.order_by = order_by
83 83 self.reversed = (order_by or '').startswith('-')
84 84 if not perm_set:
85 85 perm_set = ['repository.read', 'repository.write',
86 86 'repository.admin']
87 87 self.perm_set = perm_set
88 88
89 89 def __len__(self):
90 90 return len(self.db_repo_list)
91 91
92 92 def __repr__(self):
93 93 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
94 94
95 95 def __iter__(self):
96 96 for dbr in self.db_repo_list:
97 97 # check permission at this level
98 98 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 99 dbr.repo_name, 'SimpleCachedRepoList check')
100 100 if not has_perm:
101 101 continue
102 102
103 103 tmp_d = {
104 104 'name': dbr.repo_name,
105 105 'dbrepo': dbr.get_dict(),
106 106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 107 }
108 108 yield tmp_d
109 109
110 110
111 111 class _PermCheckIterator(object):
112 112
113 113 def __init__(
114 114 self, obj_list, obj_attr, perm_set, perm_checker,
115 115 extra_kwargs=None):
116 116 """
117 117 Creates iterator from given list of objects, additionally
118 118 checking permission for them from perm_set var
119 119
120 120 :param obj_list: list of db objects
121 121 :param obj_attr: attribute of object to pass into perm_checker
122 122 :param perm_set: list of permissions to check
123 123 :param perm_checker: callable to check permissions against
124 124 """
125 125 self.obj_list = obj_list
126 126 self.obj_attr = obj_attr
127 127 self.perm_set = perm_set
128 128 self.perm_checker = perm_checker
129 129 self.extra_kwargs = extra_kwargs or {}
130 130
131 131 def __len__(self):
132 132 return len(self.obj_list)
133 133
134 134 def __repr__(self):
135 135 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
136 136
137 137 def __iter__(self):
138 138 checker = self.perm_checker(*self.perm_set)
139 139 for db_obj in self.obj_list:
140 140 # check permission at this level
141 141 name = getattr(db_obj, self.obj_attr, None)
142 142 if not checker(name, self.__class__.__name__, **self.extra_kwargs):
143 143 continue
144 144
145 145 yield db_obj
146 146
147 147
148 148 class RepoList(_PermCheckIterator):
149 149
150 150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 151 if not perm_set:
152 152 perm_set = [
153 153 'repository.read', 'repository.write', 'repository.admin']
154 154
155 155 super(RepoList, self).__init__(
156 156 obj_list=db_repo_list,
157 157 obj_attr='repo_name', perm_set=perm_set,
158 158 perm_checker=HasRepoPermissionAny,
159 159 extra_kwargs=extra_kwargs)
160 160
161 161
162 162 class RepoGroupList(_PermCheckIterator):
163 163
164 164 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
165 165 if not perm_set:
166 166 perm_set = ['group.read', 'group.write', 'group.admin']
167 167
168 168 super(RepoGroupList, self).__init__(
169 169 obj_list=db_repo_group_list,
170 170 obj_attr='group_name', perm_set=perm_set,
171 171 perm_checker=HasRepoGroupPermissionAny,
172 172 extra_kwargs=extra_kwargs)
173 173
174 174
175 175 class UserGroupList(_PermCheckIterator):
176 176
177 177 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
178 178 if not perm_set:
179 179 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
180 180
181 181 super(UserGroupList, self).__init__(
182 182 obj_list=db_user_group_list,
183 183 obj_attr='users_group_name', perm_set=perm_set,
184 184 perm_checker=HasUserGroupPermissionAny,
185 185 extra_kwargs=extra_kwargs)
186 186
187 187
188 188 class ScmModel(BaseModel):
189 189 """
190 190 Generic Scm Model
191 191 """
192 192
193 193 @LazyProperty
194 194 def repos_path(self):
195 195 """
196 196 Gets the repositories root path from database
197 197 """
198 198
199 199 settings_model = VcsSettingsModel(sa=self.sa)
200 200 return settings_model.get_repos_location()
201 201
202 202 def repo_scan(self, repos_path=None):
203 203 """
204 204 Listing of repositories in given path. This path should not be a
205 205 repository itself. Return a dictionary of repository objects
206 206
207 207 :param repos_path: path to directory containing repositories
208 208 """
209 209
210 210 if repos_path is None:
211 211 repos_path = self.repos_path
212 212
213 213 log.info('scanning for repositories in %s', repos_path)
214 214
215 215 config = make_db_config()
216 216 config.set('extensions', 'largefiles', '')
217 217 repos = {}
218 218
219 219 for name, path in get_filesystem_repos(repos_path, recursive=True):
220 220 # name need to be decomposed and put back together using the /
221 221 # since this is internal storage separator for rhodecode
222 222 name = Repository.normalize_repo_name(name)
223 223
224 224 try:
225 225 if name in repos:
226 226 raise RepositoryError('Duplicate repository name %s '
227 227 'found in %s' % (name, path))
228 228 elif path[0] in rhodecode.BACKENDS:
229 229 klass = get_backend(path[0])
230 230 repos[name] = klass(path[1], config=config)
231 231 except OSError:
232 232 continue
233 233 log.debug('found %s paths with repositories', len(repos))
234 234 return repos
235 235
236 236 def get_repos(self, all_repos=None, sort_key=None):
237 237 """
238 238 Get all repositories from db and for each repo create it's
239 239 backend instance and fill that backed with information from database
240 240
241 241 :param all_repos: list of repository names as strings
242 242 give specific repositories list, good for filtering
243 243
244 244 :param sort_key: initial sorting of repositories
245 245 """
246 246 if all_repos is None:
247 247 all_repos = self.sa.query(Repository)\
248 248 .filter(Repository.group_id == None)\
249 249 .order_by(func.lower(Repository.repo_name)).all()
250 250 repo_iter = SimpleCachedRepoList(
251 251 all_repos, repos_path=self.repos_path, order_by=sort_key)
252 252 return repo_iter
253 253
254 254 def get_repo_groups(self, all_groups=None):
255 255 if all_groups is None:
256 256 all_groups = RepoGroup.query()\
257 257 .filter(RepoGroup.group_parent_id == None).all()
258 258 return [x for x in RepoGroupList(all_groups)]
259 259
260 260 def mark_for_invalidation(self, repo_name, delete=False):
261 261 """
262 262 Mark caches of this repo invalid in the database. `delete` flag
263 263 removes the cache entries
264 264
265 265 :param repo_name: the repo_name for which caches should be marked
266 266 invalid, or deleted
267 267 :param delete: delete the entry keys instead of setting bool
268 268 flag on them, and also purge caches used by the dogpile
269 269 """
270 270 repo = Repository.get_by_repo_name(repo_name)
271 271
272 272 if repo:
273 273 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
274 274 repo_id=repo.repo_id)
275 275 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
276 276
277 277 repo_id = repo.repo_id
278 278 config = repo._config
279 279 config.set('extensions', 'largefiles', '')
280 280 repo.update_commit_cache(config=config, cs_cache=None)
281 281 if delete:
282 282 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
283 283 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
284 284
285 285 def toggle_following_repo(self, follow_repo_id, user_id):
286 286
287 287 f = self.sa.query(UserFollowing)\
288 288 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
289 289 .filter(UserFollowing.user_id == user_id).scalar()
290 290
291 291 if f is not None:
292 292 try:
293 293 self.sa.delete(f)
294 294 return
295 295 except Exception:
296 296 log.error(traceback.format_exc())
297 297 raise
298 298
299 299 try:
300 300 f = UserFollowing()
301 301 f.user_id = user_id
302 302 f.follows_repo_id = follow_repo_id
303 303 self.sa.add(f)
304 304 except Exception:
305 305 log.error(traceback.format_exc())
306 306 raise
307 307
308 308 def toggle_following_user(self, follow_user_id, user_id):
309 309 f = self.sa.query(UserFollowing)\
310 310 .filter(UserFollowing.follows_user_id == follow_user_id)\
311 311 .filter(UserFollowing.user_id == user_id).scalar()
312 312
313 313 if f is not None:
314 314 try:
315 315 self.sa.delete(f)
316 316 return
317 317 except Exception:
318 318 log.error(traceback.format_exc())
319 319 raise
320 320
321 321 try:
322 322 f = UserFollowing()
323 323 f.user_id = user_id
324 324 f.follows_user_id = follow_user_id
325 325 self.sa.add(f)
326 326 except Exception:
327 327 log.error(traceback.format_exc())
328 328 raise
329 329
330 330 def is_following_repo(self, repo_name, user_id, cache=False):
331 331 r = self.sa.query(Repository)\
332 332 .filter(Repository.repo_name == repo_name).scalar()
333 333
334 334 f = self.sa.query(UserFollowing)\
335 335 .filter(UserFollowing.follows_repository == r)\
336 336 .filter(UserFollowing.user_id == user_id).scalar()
337 337
338 338 return f is not None
339 339
340 340 def is_following_user(self, username, user_id, cache=False):
341 341 u = User.get_by_username(username)
342 342
343 343 f = self.sa.query(UserFollowing)\
344 344 .filter(UserFollowing.follows_user == u)\
345 345 .filter(UserFollowing.user_id == user_id).scalar()
346 346
347 347 return f is not None
348 348
349 349 def get_followers(self, repo):
350 350 repo = self._get_repo(repo)
351 351
352 352 return self.sa.query(UserFollowing)\
353 353 .filter(UserFollowing.follows_repository == repo).count()
354 354
355 355 def get_forks(self, repo):
356 356 repo = self._get_repo(repo)
357 357 return self.sa.query(Repository)\
358 358 .filter(Repository.fork == repo).count()
359 359
360 360 def get_pull_requests(self, repo):
361 361 repo = self._get_repo(repo)
362 362 return self.sa.query(PullRequest)\
363 363 .filter(PullRequest.target_repo == repo)\
364 364 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
365 365
366 366 def mark_as_fork(self, repo, fork, user):
367 367 repo = self._get_repo(repo)
368 368 fork = self._get_repo(fork)
369 369 if fork and repo.repo_id == fork.repo_id:
370 370 raise Exception("Cannot set repository as fork of itself")
371 371
372 372 if fork and repo.repo_type != fork.repo_type:
373 373 raise RepositoryError(
374 374 "Cannot set repository as fork of repository with other type")
375 375
376 376 repo.fork = fork
377 377 self.sa.add(repo)
378 378 return repo
379 379
380 380 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
381 381 dbrepo = self._get_repo(repo)
382 382 remote_uri = remote_uri or dbrepo.clone_uri
383 383 if not remote_uri:
384 384 raise Exception("This repository doesn't have a clone uri")
385 385
386 386 repo = dbrepo.scm_instance(cache=False)
387 387 repo.config.clear_section('hooks')
388 388
389 389 try:
390 390 # NOTE(marcink): add extra validation so we skip invalid urls
391 391 # this is due this tasks can be executed via scheduler without
392 392 # proper validation of remote_uri
393 393 if validate_uri:
394 394 config = make_db_config(clear_session=False)
395 395 url_validator(remote_uri, dbrepo.repo_type, config)
396 396 except InvalidCloneUrl:
397 397 raise
398 398
399 399 repo_name = dbrepo.repo_name
400 400 try:
401 401 # TODO: we need to make sure those operations call proper hooks !
402 402 repo.fetch(remote_uri)
403 403
404 404 self.mark_for_invalidation(repo_name)
405 405 except Exception:
406 406 log.error(traceback.format_exc())
407 407 raise
408 408
409 409 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
410 410 dbrepo = self._get_repo(repo)
411 411 remote_uri = remote_uri or dbrepo.push_uri
412 412 if not remote_uri:
413 413 raise Exception("This repository doesn't have a clone uri")
414 414
415 415 repo = dbrepo.scm_instance(cache=False)
416 416 repo.config.clear_section('hooks')
417 417
418 418 try:
419 419 # NOTE(marcink): add extra validation so we skip invalid urls
420 420 # this is due this tasks can be executed via scheduler without
421 421 # proper validation of remote_uri
422 422 if validate_uri:
423 423 config = make_db_config(clear_session=False)
424 424 url_validator(remote_uri, dbrepo.repo_type, config)
425 425 except InvalidCloneUrl:
426 426 raise
427 427
428 428 try:
429 429 repo.push(remote_uri)
430 430 except Exception:
431 431 log.error(traceback.format_exc())
432 432 raise
433 433
434 434 def commit_change(self, repo, repo_name, commit, user, author, message,
435 435 content, f_path):
436 436 """
437 437 Commits changes
438 438
439 439 :param repo: SCM instance
440 440
441 441 """
442 442 user = self._get_user(user)
443 443
444 444 # decoding here will force that we have proper encoded values
445 445 # in any other case this will throw exceptions and deny commit
446 446 content = safe_str(content)
447 447 path = safe_str(f_path)
448 448 # message and author needs to be unicode
449 449 # proper backend should then translate that into required type
450 450 message = safe_unicode(message)
451 451 author = safe_unicode(author)
452 452 imc = repo.in_memory_commit
453 453 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
454 454 try:
455 455 # TODO: handle pre-push action !
456 456 tip = imc.commit(
457 457 message=message, author=author, parents=[commit],
458 458 branch=commit.branch)
459 459 except Exception as e:
460 460 log.error(traceback.format_exc())
461 461 raise IMCCommitError(str(e))
462 462 finally:
463 463 # always clear caches, if commit fails we want fresh object also
464 464 self.mark_for_invalidation(repo_name)
465 465
466 466 # We trigger the post-push action
467 467 hooks_utils.trigger_post_push_hook(
468 468 username=user.username, action='push_local', hook_type='post_push',
469 469 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
470 470 return tip
471 471
472 472 def _sanitize_path(self, f_path):
473 473 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
474 474 raise NonRelativePathError('%s is not an relative path' % f_path)
475 475 if f_path:
476 476 f_path = os.path.normpath(f_path)
477 477 return f_path
478 478
479 479 def get_dirnode_metadata(self, request, commit, dir_node):
480 480 if not dir_node.is_dir():
481 481 return []
482 482
483 483 data = []
484 484 for node in dir_node:
485 485 if not node.is_file():
486 486 # we skip file-nodes
487 487 continue
488 488
489 489 last_commit = node.last_commit
490 490 last_commit_date = last_commit.date
491 491 data.append({
492 492 'name': node.name,
493 493 'size': h.format_byte_size_binary(node.size),
494 494 'modified_at': h.format_date(last_commit_date),
495 495 'modified_ts': last_commit_date.isoformat(),
496 496 'revision': last_commit.revision,
497 497 'short_id': last_commit.short_id,
498 498 'message': h.escape(last_commit.message),
499 499 'author': h.escape(last_commit.author),
500 500 'user_profile': h.gravatar_with_user(
501 501 request, last_commit.author),
502 502 })
503 503
504 504 return data
505 505
506 506 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
507 507 extended_info=False, content=False, max_file_bytes=None):
508 508 """
509 509 recursive walk in root dir and return a set of all path in that dir
510 510 based on repository walk function
511 511
512 512 :param repo_name: name of repository
513 513 :param commit_id: commit id for which to list nodes
514 514 :param root_path: root path to list
515 515 :param flat: return as a list, if False returns a dict with description
516 516 :param extended_info: show additional info such as md5, binary, size etc
517 517 :param content: add nodes content to the return data
518 518 :param max_file_bytes: will not return file contents over this limit
519 519
520 520 """
521 521 _files = list()
522 522 _dirs = list()
523 523 try:
524 524 _repo = self._get_repo(repo_name)
525 525 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
526 526 root_path = root_path.lstrip('/')
527 527 for __, dirs, files in commit.walk(root_path):
528 528
529 529 for f in files:
530 530 _content = None
531 531 _data = f_name = f.unicode_path
532 532
533 533 if not flat:
534 534 _data = {
535 535 "name": h.escape(f_name),
536 536 "type": "file",
537 537 }
538 538 if extended_info:
539 539 _data.update({
540 540 "md5": f.md5,
541 541 "binary": f.is_binary,
542 542 "size": f.size,
543 543 "extension": f.extension,
544 544 "mimetype": f.mimetype,
545 545 "lines": f.lines()[0]
546 546 })
547 547
548 548 if content:
549 549 over_size_limit = (max_file_bytes is not None
550 550 and f.size > max_file_bytes)
551 551 full_content = None
552 552 if not f.is_binary and not over_size_limit:
553 553 full_content = safe_str(f.content)
554 554
555 555 _data.update({
556 556 "content": full_content,
557 557 })
558 558 _files.append(_data)
559 559
560 560 for d in dirs:
561 561 _data = d_name = d.unicode_path
562 562 if not flat:
563 563 _data = {
564 564 "name": h.escape(d_name),
565 565 "type": "dir",
566 566 }
567 567 if extended_info:
568 568 _data.update({
569 569 "md5": None,
570 570 "binary": None,
571 571 "size": None,
572 572 "extension": None,
573 573 })
574 574 if content:
575 575 _data.update({
576 576 "content": None
577 577 })
578 578 _dirs.append(_data)
579 579 except RepositoryError:
580 580 log.exception("Exception in get_nodes")
581 581 raise
582 582
583 583 return _dirs, _files
584 584
585 585 def get_node(self, repo_name, commit_id, file_path,
586 586 extended_info=False, content=False, max_file_bytes=None, cache=True):
587 587 """
588 588 retrieve single node from commit
589 589 """
590 590 try:
591 591
592 592 _repo = self._get_repo(repo_name)
593 593 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
594 594
595 595 file_node = commit.get_node(file_path)
596 596 if file_node.is_dir():
597 597 raise RepositoryError('The given path is a directory')
598 598
599 599 _content = None
600 600 f_name = file_node.unicode_path
601 601
602 602 file_data = {
603 603 "name": h.escape(f_name),
604 604 "type": "file",
605 605 }
606 606
607 607 if extended_info:
608 608 file_data.update({
609 609 "extension": file_node.extension,
610 610 "mimetype": file_node.mimetype,
611 611 })
612 612
613 613 if cache:
614 614 md5 = file_node.md5
615 615 is_binary = file_node.is_binary
616 616 size = file_node.size
617 617 else:
618 618 is_binary, md5, size, _content = file_node.metadata_uncached()
619 619
620 620 file_data.update({
621 621 "md5": md5,
622 622 "binary": is_binary,
623 623 "size": size,
624 624 })
625 625
626 626 if content and cache:
627 627 # get content + cache
628 628 size = file_node.size
629 629 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
630 630 full_content = None
631 631 if not file_node.is_binary and not over_size_limit:
632 632 full_content = safe_unicode(file_node.content)
633 633
634 634 file_data.update({
635 635 "content": full_content,
636 636 })
637 637 elif content:
638 638 # get content *without* cache
639 639 if _content is None:
640 640 is_binary, md5, size, _content = file_node.metadata_uncached()
641 641
642 642 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
643 643 full_content = None
644 644 if not is_binary and not over_size_limit:
645 645 full_content = safe_unicode(_content)
646 646
647 647 file_data.update({
648 648 "content": full_content,
649 649 })
650 650
651 651 except RepositoryError:
652 652 log.exception("Exception in get_node")
653 653 raise
654 654
655 655 return file_data
656 656
657 657 def get_fts_data(self, repo_name, commit_id, root_path='/'):
658 658 """
659 659 Fetch node tree for usage in full text search
660 660 """
661 661
662 662 tree_info = list()
663 663
664 664 try:
665 665 _repo = self._get_repo(repo_name)
666 666 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
667 667 root_path = root_path.lstrip('/')
668 668 for __, dirs, files in commit.walk(root_path):
669 669
670 670 for f in files:
671 671 is_binary, md5, size, _content = f.metadata_uncached()
672 672 _data = {
673 673 "name": f.unicode_path,
674 674 "md5": md5,
675 675 "extension": f.extension,
676 676 "binary": is_binary,
677 677 "size": size
678 678 }
679 679
680 680 tree_info.append(_data)
681 681
682 682 except RepositoryError:
683 683 log.exception("Exception in get_nodes")
684 684 raise
685 685
686 686 return tree_info
687 687
688 688 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
689 689 author=None, trigger_push_hook=True):
690 690 """
691 691 Commits given multiple nodes into repo
692 692
693 693 :param user: RhodeCode User object or user_id, the commiter
694 694 :param repo: RhodeCode Repository object
695 695 :param message: commit message
696 696 :param nodes: mapping {filename:{'content':content},...}
697 697 :param parent_commit: parent commit, can be empty than it's
698 698 initial commit
699 699 :param author: author of commit, cna be different that commiter
700 700 only for git
701 701 :param trigger_push_hook: trigger push hooks
702 702
703 703 :returns: new commited commit
704 704 """
705 705
706 706 user = self._get_user(user)
707 707 scm_instance = repo.scm_instance(cache=False)
708 708
709 709 processed_nodes = []
710 710 for f_path in nodes:
711 711 f_path = self._sanitize_path(f_path)
712 712 content = nodes[f_path]['content']
713 713 f_path = safe_str(f_path)
714 714 # decoding here will force that we have proper encoded values
715 715 # in any other case this will throw exceptions and deny commit
716 716 if isinstance(content, (basestring,)):
717 717 content = safe_str(content)
718 718 elif isinstance(content, (file, cStringIO.OutputType,)):
719 719 content = content.read()
720 720 else:
721 721 raise Exception('Content is of unrecognized type %s' % (
722 722 type(content)
723 723 ))
724 724 processed_nodes.append((f_path, content))
725 725
726 726 message = safe_unicode(message)
727 727 commiter = user.full_contact
728 728 author = safe_unicode(author) if author else commiter
729 729
730 730 imc = scm_instance.in_memory_commit
731 731
732 732 if not parent_commit:
733 733 parent_commit = EmptyCommit(alias=scm_instance.alias)
734 734
735 735 if isinstance(parent_commit, EmptyCommit):
736 736 # EmptyCommit means we we're editing empty repository
737 737 parents = None
738 738 else:
739 739 parents = [parent_commit]
740 740 # add multiple nodes
741 741 for path, content in processed_nodes:
742 742 imc.add(FileNode(path, content=content))
743 743 # TODO: handle pre push scenario
744 744 tip = imc.commit(message=message,
745 745 author=author,
746 746 parents=parents,
747 747 branch=parent_commit.branch)
748 748
749 749 self.mark_for_invalidation(repo.repo_name)
750 750 if trigger_push_hook:
751 751 hooks_utils.trigger_post_push_hook(
752 752 username=user.username, action='push_local',
753 753 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
754 754 hook_type='post_push',
755 755 commit_ids=[tip.raw_id])
756 756 return tip
757 757
758 758 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
759 759 author=None, trigger_push_hook=True):
760 760 user = self._get_user(user)
761 761 scm_instance = repo.scm_instance(cache=False)
762 762
763 763 message = safe_unicode(message)
764 764 commiter = user.full_contact
765 765 author = safe_unicode(author) if author else commiter
766 766
767 767 imc = scm_instance.in_memory_commit
768 768
769 769 if not parent_commit:
770 770 parent_commit = EmptyCommit(alias=scm_instance.alias)
771 771
772 772 if isinstance(parent_commit, EmptyCommit):
773 773 # EmptyCommit means we we're editing empty repository
774 774 parents = None
775 775 else:
776 776 parents = [parent_commit]
777 777
778 778 # add multiple nodes
779 779 for _filename, data in nodes.items():
780 780 # new filename, can be renamed from the old one, also sanitaze
781 781 # the path for any hack around relative paths like ../../ etc.
782 782 filename = self._sanitize_path(data['filename'])
783 783 old_filename = self._sanitize_path(_filename)
784 784 content = data['content']
785 785 file_mode = data.get('mode')
786 786 filenode = FileNode(old_filename, content=content, mode=file_mode)
787 787 op = data['op']
788 788 if op == 'add':
789 789 imc.add(filenode)
790 790 elif op == 'del':
791 791 imc.remove(filenode)
792 792 elif op == 'mod':
793 793 if filename != old_filename:
794 794 # TODO: handle renames more efficient, needs vcs lib changes
795 795 imc.remove(filenode)
796 796 imc.add(FileNode(filename, content=content, mode=file_mode))
797 797 else:
798 798 imc.change(filenode)
799 799
800 800 try:
801 801 # TODO: handle pre push scenario commit changes
802 802 tip = imc.commit(message=message,
803 803 author=author,
804 804 parents=parents,
805 805 branch=parent_commit.branch)
806 806 except NodeNotChangedError:
807 807 raise
808 808 except Exception as e:
809 809 log.exception("Unexpected exception during call to imc.commit")
810 810 raise IMCCommitError(str(e))
811 811 finally:
812 812 # always clear caches, if commit fails we want fresh object also
813 813 self.mark_for_invalidation(repo.repo_name)
814 814
815 815 if trigger_push_hook:
816 816 hooks_utils.trigger_post_push_hook(
817 817 username=user.username, action='push_local', hook_type='post_push',
818 818 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
819 819 commit_ids=[tip.raw_id])
820 820
821 return tip
822
821 823 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
822 824 author=None, trigger_push_hook=True):
823 825 """
824 826 Deletes given multiple nodes into `repo`
825 827
826 828 :param user: RhodeCode User object or user_id, the committer
827 829 :param repo: RhodeCode Repository object
828 830 :param message: commit message
829 831 :param nodes: mapping {filename:{'content':content},...}
830 832 :param parent_commit: parent commit, can be empty than it's initial
831 833 commit
832 834 :param author: author of commit, cna be different that commiter only
833 835 for git
834 836 :param trigger_push_hook: trigger push hooks
835 837
836 838 :returns: new commit after deletion
837 839 """
838 840
839 841 user = self._get_user(user)
840 842 scm_instance = repo.scm_instance(cache=False)
841 843
842 844 processed_nodes = []
843 845 for f_path in nodes:
844 846 f_path = self._sanitize_path(f_path)
845 847 # content can be empty but for compatabilty it allows same dicts
846 848 # structure as add_nodes
847 849 content = nodes[f_path].get('content')
848 850 processed_nodes.append((f_path, content))
849 851
850 852 message = safe_unicode(message)
851 853 commiter = user.full_contact
852 854 author = safe_unicode(author) if author else commiter
853 855
854 856 imc = scm_instance.in_memory_commit
855 857
856 858 if not parent_commit:
857 859 parent_commit = EmptyCommit(alias=scm_instance.alias)
858 860
859 861 if isinstance(parent_commit, EmptyCommit):
860 862 # EmptyCommit means we we're editing empty repository
861 863 parents = None
862 864 else:
863 865 parents = [parent_commit]
864 866 # add multiple nodes
865 867 for path, content in processed_nodes:
866 868 imc.remove(FileNode(path, content=content))
867 869
868 870 # TODO: handle pre push scenario
869 871 tip = imc.commit(message=message,
870 872 author=author,
871 873 parents=parents,
872 874 branch=parent_commit.branch)
873 875
874 876 self.mark_for_invalidation(repo.repo_name)
875 877 if trigger_push_hook:
876 878 hooks_utils.trigger_post_push_hook(
877 879 username=user.username, action='push_local', hook_type='post_push',
878 880 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
879 881 commit_ids=[tip.raw_id])
880 882 return tip
881 883
882 884 def strip(self, repo, commit_id, branch):
883 885 scm_instance = repo.scm_instance(cache=False)
884 886 scm_instance.config.clear_section('hooks')
885 887 scm_instance.strip(commit_id, branch)
886 888 self.mark_for_invalidation(repo.repo_name)
887 889
888 890 def get_unread_journal(self):
889 891 return self.sa.query(UserLog).count()
890 892
891 893 def get_repo_landing_revs(self, translator, repo=None):
892 894 """
893 895 Generates select option with tags branches and bookmarks (for hg only)
894 896 grouped by type
895 897
896 898 :param repo:
897 899 """
898 900 _ = translator
899 901 repo = self._get_repo(repo)
900 902
901 903 hist_l = [
902 904 ['rev:tip', _('latest tip')]
903 905 ]
904 906 choices = [
905 907 'rev:tip'
906 908 ]
907 909
908 910 if not repo:
909 911 return choices, hist_l
910 912
911 913 repo = repo.scm_instance()
912 914
913 915 branches_group = (
914 916 [(u'branch:%s' % safe_unicode(b), safe_unicode(b))
915 917 for b in repo.branches],
916 918 _("Branches"))
917 919 hist_l.append(branches_group)
918 920 choices.extend([x[0] for x in branches_group[0]])
919 921
920 922 if repo.alias == 'hg':
921 923 bookmarks_group = (
922 924 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
923 925 for b in repo.bookmarks],
924 926 _("Bookmarks"))
925 927 hist_l.append(bookmarks_group)
926 928 choices.extend([x[0] for x in bookmarks_group[0]])
927 929
928 930 tags_group = (
929 931 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
930 932 for t in repo.tags],
931 933 _("Tags"))
932 934 hist_l.append(tags_group)
933 935 choices.extend([x[0] for x in tags_group[0]])
934 936
935 937 return choices, hist_l
936 938
937 939 def get_server_info(self, environ=None):
938 940 server_info = get_system_info(environ)
939 941 return server_info
@@ -1,418 +1,418 b''
1 1 /* BASICS */
2 2
3 3 .CodeMirror {
4 4 /* Set height, width, borders, and global font properties here */
5 5 font-family: monospace;
6 6 height: 300px;
7 7 color: black;
8 8 border-radius: @border-radius;
9 9 border: @border-thickness solid @grey6;
10 10 margin: 0 0 @padding;
11 11 }
12 12
13 13 /* PADDING */
14 14
15 15 .CodeMirror-lines {
16 16 padding: 4px 0; /* Vertical padding around content */
17 17 }
18 18 .CodeMirror pre {
19 19 padding: 0 4px; /* Horizontal padding of content */
20 20 }
21 21
22 22 .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
23 23 background-color: white; /* The little square between H and V scrollbars */
24 24 }
25 25
26 26 /* GUTTER */
27 27
28 28 .CodeMirror-gutters {
29 29 border-right: 1px solid #ddd;
30 background-color: @grey6;
30 background-color: white;
31 31 white-space: nowrap;
32 32 }
33 33 .CodeMirror-linenumbers {}
34 34 .CodeMirror-linenumber {
35 35 padding: 0 3px 0 5px;
36 36 min-width: 20px;
37 37 text-align: right;
38 38 color: @grey4;
39 39 white-space: nowrap;
40 40 }
41 41
42 42 .CodeMirror-guttermarker { color: black; }
43 43 .CodeMirror-guttermarker-subtle { color: #999; }
44 44
45 45 /* CURSOR */
46 46
47 47 .CodeMirror div.CodeMirror-cursor {
48 48 border-left: 1px solid black;
49 49 }
50 50 /* Shown when moving in bi-directional text */
51 51 .CodeMirror div.CodeMirror-secondarycursor {
52 52 border-left: 1px solid silver;
53 53 }
54 54 .CodeMirror.cm-fat-cursor div.CodeMirror-cursor {
55 55 width: auto;
56 56 border: 0;
57 57 background: @grey6;
58 58 }
59 59 .CodeMirror.cm-fat-cursor div.CodeMirror-cursors {
60 60 z-index: 1;
61 61 }
62 62
63 63 .cm-animate-fat-cursor {
64 64 width: auto;
65 65 border: 0;
66 66 -webkit-animation: blink 1.06s steps(1) infinite;
67 67 -moz-animation: blink 1.06s steps(1) infinite;
68 68 animation: blink 1.06s steps(1) infinite;
69 69 }
70 70 @-moz-keyframes blink {
71 71 0% { background: #7e7; }
72 72 50% { background: none; }
73 73 100% { background: #7e7; }
74 74 }
75 75 @-webkit-keyframes blink {
76 76 0% { background: #7e7; }
77 77 50% { background: none; }
78 78 100% { background: #7e7; }
79 79 }
80 80 @keyframes blink {
81 81 0% { background: #7e7; }
82 82 50% { background: none; }
83 83 100% { background: #7e7; }
84 84 }
85 85
86 86 /* Can style cursor different in overwrite (non-insert) mode */
87 87 div.CodeMirror-overwrite div.CodeMirror-cursor {}
88 88
89 89 .cm-tab { display: inline-block; text-decoration: inherit; }
90 90
91 91 .CodeMirror-ruler {
92 92 border-left: 1px solid #ccc;
93 93 position: absolute;
94 94 }
95 95
96 96 /* DEFAULT THEME */
97 97
98 98 .cm-s-default .cm-header {color: blue;}
99 99 .cm-s-default .cm-quote {color: #090;}
100 100 .cm-negative {color: #d44;}
101 101 .cm-positive {color: #292;}
102 102 .cm-header, .cm-strong {font-weight: bold;}
103 103 .cm-em {font-style: italic;}
104 104 .cm-link {text-decoration: underline;}
105 105 .cm-strikethrough {text-decoration: line-through;}
106 106
107 107 .cm-s-default .cm-keyword {color: #708;}
108 108 .cm-s-default .cm-atom {color: #219;}
109 109 .cm-s-default .cm-number {color: #164;}
110 110 .cm-s-default .cm-def {color: #00f;}
111 111 .cm-s-default .cm-variable,
112 112 .cm-s-default .cm-punctuation,
113 113 .cm-s-default .cm-property,
114 114 .cm-s-default .cm-operator {}
115 115 .cm-s-default .cm-variable-2 {color: #05a;}
116 116 .cm-s-default .cm-variable-3 {color: #085;}
117 117 .cm-s-default .cm-comment {color: #a50;}
118 118 .cm-s-default .cm-string {color: #a11;}
119 119 .cm-s-default .cm-string-2 {color: #f50;}
120 120 .cm-s-default .cm-meta {color: #555;}
121 121 .cm-s-default .cm-qualifier {color: #555;}
122 122 .cm-s-default .cm-builtin {color: #30a;}
123 123 .cm-s-default .cm-bracket {color: #997;}
124 124 .cm-s-default .cm-tag {color: #170;}
125 125 .cm-s-default .cm-attribute {color: #00c;}
126 126 .cm-s-default .cm-hr {color: #999;}
127 127 .cm-s-default .cm-link {color: #00c;}
128 128
129 129 .cm-s-default .cm-error {color: #f00;}
130 130 .cm-invalidchar {color: #f00;}
131 131
132 132 .CodeMirror-composing { border-bottom: 2px solid; }
133 133
134 134 /* Default styles for common addons */
135 135
136 136 div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
137 137 div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
138 138 .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
139 139 .CodeMirror-activeline-background {background: #e8f2ff;}
140 140
141 141 /* STOP */
142 142
143 143 /* The rest of this file contains styles related to the mechanics of
144 144 the editor. You probably shouldn't touch them. */
145 145
146 146 .CodeMirror {
147 147 position: relative;
148 148 overflow: hidden;
149 149 background: white;
150 150 }
151 151
152 152 .CodeMirror-scroll {
153 153 overflow: scroll !important; /* Things will break if this is overridden */
154 154 /* 30px is the magic margin used to hide the element's real scrollbars */
155 155 /* See overflow: hidden in .CodeMirror */
156 156 margin-bottom: -30px; margin-right: -30px;
157 157 padding-bottom: 30px;
158 158 height: 100%;
159 159 outline: none; /* Prevent dragging from highlighting the element */
160 160 position: relative;
161 161 }
162 162 .CodeMirror-sizer {
163 163 position: relative;
164 164 border-right: 30px solid transparent;
165 165 }
166 166
167 167 /* The fake, visible scrollbars. Used to force redraw during scrolling
168 168 before actual scrolling happens, thus preventing shaking and
169 169 flickering artifacts. */
170 170 .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
171 171 position: absolute;
172 172 z-index: 6;
173 173 display: none;
174 174 }
175 175 .CodeMirror-vscrollbar {
176 176 right: 0; top: 0;
177 177 overflow-x: hidden;
178 178 overflow-y: scroll;
179 179 }
180 180 .CodeMirror-hscrollbar {
181 181 bottom: 0; left: 0;
182 182 overflow-y: hidden;
183 183 overflow-x: scroll;
184 184 }
185 185 .CodeMirror-scrollbar-filler {
186 186 right: 0; bottom: 0;
187 187 }
188 188 .CodeMirror-gutter-filler {
189 189 left: 0; bottom: 0;
190 190 }
191 191
192 192 .CodeMirror-gutters {
193 193 position: absolute; left: 0; top: 0;
194 194 z-index: 3;
195 195 }
196 196 .CodeMirror-gutter {
197 197 white-space: normal;
198 198 height: 100%;
199 199 display: inline-block;
200 200 margin-bottom: -30px;
201 201 /* Hack to make IE7 behave */
202 202 *zoom:1;
203 203 *display:inline;
204 204 }
205 205 .CodeMirror-gutter-wrapper {
206 206 position: absolute;
207 207 z-index: 4;
208 208 height: 100%;
209 209 }
210 210 .CodeMirror-gutter-background {
211 211 position: absolute;
212 212 top: 0; bottom: 0;
213 213 z-index: 4;
214 214 }
215 215 .CodeMirror-gutter-elt {
216 216 position: absolute;
217 217 cursor: default;
218 218 z-index: 4;
219 219 }
220 220 .CodeMirror-gutter-wrapper {
221 221 -webkit-user-select: none;
222 222 -moz-user-select: none;
223 223 user-select: none;
224 224 }
225 225
226 226 .CodeMirror-lines {
227 227 cursor: text;
228 228 min-height: 1px; /* prevents collapsing before first draw */
229 229 }
230 230 .CodeMirror pre {
231 231 /* Reset some styles that the rest of the page might have set */
232 232 -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
233 233 border-width: 0;
234 234 background: transparent;
235 235 font-family: inherit;
236 236 font-size: inherit;
237 237 margin: 0;
238 238 white-space: pre;
239 239 word-wrap: normal;
240 240 line-height: inherit;
241 241 color: inherit;
242 242 z-index: 2;
243 243 position: relative;
244 244 overflow: visible;
245 245 -webkit-tap-highlight-color: transparent;
246 246 }
247 247 .CodeMirror-wrap pre {
248 248 word-wrap: break-word;
249 249 white-space: pre-wrap;
250 250 word-break: normal;
251 251 }
252 252
253 253 .CodeMirror-linebackground {
254 254 position: absolute;
255 255 left: 0; right: 0; top: 0; bottom: 0;
256 256 z-index: 0;
257 257 }
258 258
259 259 .CodeMirror-linewidget {
260 260 position: relative;
261 261 z-index: 2;
262 262 overflow: auto;
263 263 }
264 264
265 265 .CodeMirror-widget {}
266 266
267 267 .CodeMirror-code {
268 268 outline: none;
269 269 }
270 270
271 271 /* Force content-box sizing for the elements where we expect it */
272 272 .CodeMirror-scroll,
273 273 .CodeMirror-sizer,
274 274 .CodeMirror-gutter,
275 275 .CodeMirror-gutters,
276 276 .CodeMirror-linenumber {
277 277 -moz-box-sizing: content-box;
278 278 box-sizing: content-box;
279 279 }
280 280
281 281 .CodeMirror-measure {
282 282 position: absolute;
283 283 width: 100%;
284 284 height: 0;
285 285 overflow: hidden;
286 286 visibility: hidden;
287 287 }
288 288
289 289
290 290 .CodeMirror div.CodeMirror-cursor {
291 291 position: absolute;
292 292 border-right: none;
293 293 width: 0;
294 294 }
295 295
296 296 .CodeMirror-measure pre { position: static; }
297 297
298 298 div.CodeMirror-cursors {
299 299 visibility: hidden;
300 300 position: relative;
301 301 z-index: 3;
302 302 }
303 303 div.CodeMirror-dragcursors {
304 304 visibility: visible;
305 305 }
306 306
307 307 .CodeMirror-focused div.CodeMirror-cursors {
308 308 visibility: visible;
309 309 }
310 310
311 311 .CodeMirror-selected { background: #d9d9d9; }
312 312 .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
313 313 .CodeMirror-crosshair { cursor: crosshair; }
314 314 .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
315 315 .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
316 316
317 317 .cm-searching {
318 318 background: #ffa;
319 319 background: rgba(255, 255, 0, .4);
320 320 }
321 321
322 322 /* IE7 hack to prevent it from returning funny offsetTops on the spans */
323 323 .CodeMirror span { *vertical-align: text-bottom; }
324 324
325 325 /* Used to force a border model for a node */
326 326 .cm-force-border { padding-right: .1px; }
327 327
328 328 @media print {
329 329 /* Hide the cursor when printing */
330 330 .CodeMirror div.CodeMirror-cursors {
331 331 visibility: hidden;
332 332 }
333 333 }
334 334
335 335 /* See issue #2901 */
336 336 .cm-tab-wrap-hack:after { content: ''; }
337 337
338 338 /* Help users use markselection to safely style text background */
339 339 span.CodeMirror-selectedtext { background: none; }
340 340
341 341 /* codemirror autocomplete widget */
342 342 .CodeMirror-hints {
343 343 position: absolute;
344 344 z-index: 10;
345 345 overflow: hidden;
346 346 list-style: none;
347 347
348 348 margin: 0;
349 349 padding: 0;
350 350
351 351 border-radius: @border-radius;
352 352 border: @border-thickness solid @rcblue;
353 353
354 354 color: @rcblue;
355 355 background-color: white;
356 356 font-size: 95%;
357 357
358 358 max-height: 20em;
359 359 overflow-y: auto;
360 360 }
361 361
362 362 .CodeMirror-hint {
363 363 margin: 0;
364 364 padding: 4px 8px;
365 365 max-width: 40em;
366 366 white-space: pre;
367 367 color: @rcblue;
368 368 cursor: pointer;
369 369 }
370 370
371 371 .CodeMirror-hint-active {
372 372 background: @rclightblue;
373 373 color: @rcblue;
374 374 }
375 375
376 376 .CodeMirror-hint-entry {
377 377 width: 38em;
378 378 color: @rcblue;
379 379 }
380 380
381 381 .CodeMirror-hint-entry .gravatar {
382 382 height: @gravatar-size;
383 383 width: @gravatar-size;
384 384 margin-right: 4px;
385 385 }
386 386
387 387 .CodeMirror-empty {
388 388 border: @border-thickness solid @grey5;
389 389 }
390 390
391 391 .CodeMirror-focused {
392 392 border: @border-thickness solid @grey5;
393 393 }
394 394
395 395 .CodeMirror-empty.CodeMirror-focused {
396 396 border: @border-thickness solid @grey5;
397 397 }
398 398
399 399 .CodeMirror pre.CodeMirror-placeholder {
400 400 color: @grey4;
401 401 }
402 402
403 403 /** RhodeCode Customizations **/
404 404
405 405 .CodeMirror.cm-s-rc-input {
406 406 border: @border-thickness solid @grey4;
407 407 }
408 408
409 409 .CodeMirror-code pre {
410 410 border-right: 30px solid transparent;
411 411 width: -webkit-fit-content;
412 412 width: -moz-fit-content;
413 413 width: fit-content;
414 414 }
415 415 .CodeMirror-wrap .CodeMirror-code pre {
416 416 border-right: none;
417 417 width: auto;
418 418 }
@@ -1,320 +1,325 b''
1 1 // forms.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 form.rcform {
6 6
7 7 // reset for ie
8 8 // using :not(#ie) prevents older browsers from applying these rules
9 9 input[type="radio"],
10 10 input[type="checkbox"] {
11 11 padding: 0;
12 12 border: none;
13 13 }
14 14 label { display: inline; border:none; padding:0; }
15 15 .label { display: none; }
16 16
17 17 max-width: 940px;
18 18 line-height: normal;
19 19 white-space: normal;
20 20 font-size: @basefontsize;
21 21 font-family: @text-light;
22 22 color: @form-textcolor;
23 23
24 24 fieldset,
25 25 .buttons {
26 26 clear: both;
27 27 position: relative;
28 28 display:block;
29 29 width: 100%;
30 30 min-height: 3em;
31 31 margin-bottom: @form-vertical-margin;
32 32 line-height: 1.2em;
33 33
34 34 &:after { //clearfix
35 35 content: "";
36 36 clear: both;
37 37 width: 100%;
38 38 height: 1em;
39 39 }
40 40
41 41 .label:not(#ie) {
42 42 display: inline;
43 43 margin: 0 1em 0 .5em;
44 44 line-height: 1em;
45 45 }
46 46 }
47 47
48 48 legend {
49 49 float: left;
50 50 display: block;
51 51 width: @legend-width;
52 52 margin: 0;
53 53 padding: 0 @padding 0 0;
54 54 }
55 55
56 56 .fields {
57 57 float: left;
58 58 display: block;
59 59 width: 100%;
60 60 max-width: 500px;
61 61 margin: 0 0 @padding -@legend-width;
62 62 padding: 0 0 0 @legend-width;
63 63
64 64 .btn {
65 65 display: inline-block;
66 66 margin: 0 1em @padding 0;
67 67 }
68 68 }
69 69
70 70 input,
71 71 textarea {
72 72 float: left;
73 73 .box-sizing(content-box);
74 74 padding: @input-padding;
75 75 border: @border-thickness-inputs solid @grey4;
76 76 }
77 77
78 78 input {
79 79 float: left;
80 80 margin: 0 @input-padding 0 0;
81 81 line-height: 1em;
82 82 }
83 83
84 84 input[type="text"],
85 85 input[type="password"],
86 86 textarea {
87 87 float: left;
88 88 min-width: 200px;
89 89 margin: 0 1em @padding 0;
90 90 color: @form-textcolor;
91 91 }
92 92
93 93 input[type="text"],
94 94 input[type="password"] {
95 95 height: 1em;
96 96 }
97 97
98 98 textarea {
99 99 width: 100%;
100 100 margin-top: -1em; //so it lines up with legend
101 101 overflow: auto;
102 102 }
103 103
104 104 label:not(#ie) {
105 105 cursor: pointer;
106 106 display: inline-block;
107 107 position: relative;
108 108 background: white;
109 109 border-radius: 4px;
110 110 box-shadow: none;
111 111
112 112 &:hover::after {
113 113 opacity: 0.5;
114 114 }
115 115 }
116 116
117 117 input[type="radio"]:not(#ie),
118 118 input[type="checkbox"]:not(#ie) {
119 119 // Hide the input, but have it still be clickable
120 120 opacity: 0;
121 121 float: left;
122 122 height: 0;
123 123 width: 0;
124 124 margin: 0;
125 125 padding: 0;
126 126 }
127 127 input[type='radio'] + label:not(#ie),
128 128 input[type='checkbox'] + label:not(#ie) {
129 129 margin: 0;
130 130 clear: none;
131 131 }
132 132
133 133 input[type='radio'] + label:not(#ie) {
134 134 .circle (@form-radio-width,white);
135 135 float: left;
136 136 display: inline-block;
137 137 height: @form-radio-width;
138 138 width: @form-radio-width;
139 139 margin: 2px 6px 2px 0;
140 140 border: 1px solid @grey4;
141 141 background-color: white;
142 142 box-shadow: none;
143 143 text-indent: -9999px;
144 144 transition: none;
145 145
146 146 & + .label {
147 147 float: left;
148 148 margin-top: 7px
149 149 }
150 150 }
151 151
152 152 input[type='radio']:checked + label:not(#ie) {
153 153 margin: 0 4px 0 -2px;
154 154 padding: 3px;
155 155 border-style: double;
156 156 border-color: white;
157 157 border-width: thick;
158 158 background-color: @rcblue;
159 159 box-shadow: none;
160 160 }
161 161
162 162 input[type='checkbox'] + label:not(#ie) {
163 163 float: left;
164 164 width: @form-check-width;
165 165 height: @form-check-width;
166 166 margin: 0 5px 1em 0;
167 167 border: 1px solid @grey3;
168 168 .border-radius(@border-radius);
169 169 background-color: white;
170 170 box-shadow: none;
171 171 text-indent: -9999px;
172 172 transition: none;
173 173
174 174 &:after {
175 175 content: '';
176 176 width: 9px;
177 177 height: 5px;
178 178 position: absolute;
179 179 top: 4px;
180 180 left: 4px;
181 181 border: 3px solid @grey3;
182 182 border-top: none;
183 183 border-right: none;
184 184 background: transparent;
185 185 opacity: 0;
186 186 transform: rotate(-45deg);
187 187 filter: progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476); /* IE6,IE7 */
188 188
189 189 -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(SizingMethod='auto expand', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476)"; /* IE8 */ }
190 190
191 191 & + .label {
192 192 float: left;
193 193 margin-top: 5px
194 194 }
195 195 }
196 196
197 197 input[type=checkbox]:not(#ie) {
198 198 visibility: hidden;
199 199 &:checked + label:after {
200 200 opacity: 1;
201 201 }
202 202 }
203 203
204 204 // center checkbox and label on a drop-down
205 205 .drop-menu + select + input[type='checkbox'] + label:not(#ie) {
206 206 margin-top:10px;
207 207
208 208 & + .label {
209 209 margin-top: 15px;
210 210 }
211 211 }
212 212
213 213 .formlist {
214 214 position: relative;
215 215 float: left;
216 216 margin: 0;
217 217 padding: 0;
218 218
219 219 li {
220 220 list-style-type: none;
221 221
222 222 &:after {
223 223 content: "";
224 224 float: left;
225 225 display: block;
226 226 height: @padding;
227 227 width: 100%;
228 228 }
229 229 }
230 230 }
231 231
232 232 .drop-menu {
233 233 float: left;
234
235 & + .last-item {
236 margin: 0;
237 }
238
234 239 margin: 0 @input-padding 0 0;
235 240 }
236 241
237 242 .help-block,
238 243 .error-message {
239 244 display: block;
240 245 clear: both;
241 246 margin: @textmargin 0;
242 247 }
243 248
244 249 .error-message {
245 250 margin-top: 5px;
246 251 }
247 252
248 253 input[type=submit] {
249 254 &:extend(.btn-primary);
250 255
251 256 &:hover {
252 257 &:extend(.btn-primary:hover);
253 258 }
254 259 }
255 260
256 261 input[type=reset] {
257 262 &:extend(.btn-default);
258 263
259 264 &:hover {
260 265 &:extend(.btn-default:hover);
261 266 }
262 267 }
263 268
264 269 select,
265 270 option:checked {
266 271 background-color: @rclightblue;
267 272 }
268 273
269 274 }
270 275
271 276 .badged-field {
272 277 .user-badge {
273 278 line-height: 25px;
274 279 padding: .4em;
275 280 border-radius: @border-radius;
276 281 border-top: 1px solid @grey4;
277 282 border-left: 1px solid @grey4;
278 283 border-bottom: 1px solid @grey4;
279 284 font-size: 14px;
280 285 font-style: normal;
281 286 color: @text-light;
282 287 background: @grey7;
283 288 display: inline-block;
284 289 vertical-align: top;
285 290 cursor: default;
286 291 margin-right: -2px;
287 292 }
288 293 .badge-input-container {
289 294 display: flex;
290 295 position: relative;
291 296 }
292 297 .user-disabled {
293 298 text-decoration: line-through;
294 299 }
295 300 .badge-input-wrap {
296 301 display: inline-block;
297 302 }
298 303 }
299 304
300 305 // for situations where we wish to display the form value but not the form input
301 306 input.input-valuedisplay {
302 307 border: none;
303 308 }
304 309
305 310 // for forms which only display information
306 311 .infoform {
307 312 .fields {
308 313 .field {
309 314 label,
310 315 .label,
311 316 input,
312 317 .input {
313 318 margin-top: 0;
314 319 margin-bottom: 0;
315 320 padding-top: 0;
316 321 padding-bottom: 0;
317 322 }
318 323 }
319 324 }
320 325 }
@@ -1,2618 +1,2743 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-size: 120%;
38 38 color: white;
39 39 background-color: @alert2;
40 40 padding: 5px 0 5px 0;
41 41 font-weight: @text-semibold-weight;
42 42 font-family: @text-semibold;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .action-link.disabled {
104 104 color: @grey4;
105 105 cursor: inherit;
106 106 }
107 107
108 108 .clipboard-action {
109 109 cursor: pointer;
110 110 color: @grey4;
111 111 margin-left: 5px;
112 112
113 113 &:hover {
114 114 color: @grey2;
115 115 }
116 116 }
117 117
118 118 ul.simple-list{
119 119 list-style: none;
120 120 margin: 0;
121 121 padding: 0;
122 122 }
123 123
124 124 .main-content {
125 125 padding-bottom: @pagepadding;
126 126 }
127 127
128 128 .wide-mode-wrapper {
129 129 max-width:4000px !important;
130 130 }
131 131
132 132 .wrapper {
133 133 position: relative;
134 134 max-width: @wrapper-maxwidth;
135 135 margin: 0 auto;
136 136 }
137 137
138 138 #content {
139 139 clear: both;
140 140 padding: 0 @contentpadding;
141 141 }
142 142
143 143 .advanced-settings-fields{
144 144 input{
145 145 margin-left: @textmargin;
146 146 margin-right: @padding/2;
147 147 }
148 148 }
149 149
150 150 .cs_files_title {
151 151 margin: @pagepadding 0 0;
152 152 }
153 153
154 154 input.inline[type="file"] {
155 155 display: inline;
156 156 }
157 157
158 158 .error_page {
159 159 margin: 10% auto;
160 160
161 161 h1 {
162 162 color: @grey2;
163 163 }
164 164
165 165 .alert {
166 166 margin: @padding 0;
167 167 }
168 168
169 169 .error-branding {
170 170 color: @grey4;
171 171 font-weight: @text-semibold-weight;
172 172 font-family: @text-semibold;
173 173 }
174 174
175 175 .error_message {
176 176 font-family: @text-regular;
177 177 }
178 178
179 179 .sidebar {
180 180 min-height: 275px;
181 181 margin: 0;
182 182 padding: 0 0 @sidebarpadding @sidebarpadding;
183 183 border: none;
184 184 }
185 185
186 186 .main-content {
187 187 position: relative;
188 188 margin: 0 @sidebarpadding @sidebarpadding;
189 189 padding: 0 0 0 @sidebarpadding;
190 190 border-left: @border-thickness solid @grey5;
191 191
192 192 @media (max-width:767px) {
193 193 clear: both;
194 194 width: 100%;
195 195 margin: 0;
196 196 border: none;
197 197 }
198 198 }
199 199
200 200 .inner-column {
201 201 float: left;
202 202 width: 29.75%;
203 203 min-height: 150px;
204 204 margin: @sidebarpadding 2% 0 0;
205 205 padding: 0 2% 0 0;
206 206 border-right: @border-thickness solid @grey5;
207 207
208 208 @media (max-width:767px) {
209 209 clear: both;
210 210 width: 100%;
211 211 border: none;
212 212 }
213 213
214 214 ul {
215 215 padding-left: 1.25em;
216 216 }
217 217
218 218 &:last-child {
219 219 margin: @sidebarpadding 0 0;
220 220 border: none;
221 221 }
222 222
223 223 h4 {
224 224 margin: 0 0 @padding;
225 225 font-weight: @text-semibold-weight;
226 226 font-family: @text-semibold;
227 227 }
228 228 }
229 229 }
230 230 .error-page-logo {
231 231 width: 130px;
232 232 height: 160px;
233 233 }
234 234
235 235 // HEADER
236 236 .header {
237 237
238 238 // TODO: johbo: Fix login pages, so that they work without a min-height
239 239 // for the header and then remove the min-height. I chose a smaller value
240 240 // intentionally here to avoid rendering issues in the main navigation.
241 241 min-height: 49px;
242 242
243 243 position: relative;
244 244 vertical-align: bottom;
245 245 padding: 0 @header-padding;
246 246 background-color: @grey1;
247 247 color: @grey5;
248 248
249 249 .title {
250 250 overflow: visible;
251 251 }
252 252
253 253 &:before,
254 254 &:after {
255 255 content: "";
256 256 clear: both;
257 257 width: 100%;
258 258 }
259 259
260 260 // TODO: johbo: Avoids breaking "Repositories" chooser
261 261 .select2-container .select2-choice .select2-arrow {
262 262 display: none;
263 263 }
264 264 }
265 265
266 266 #header-inner {
267 267 &.title {
268 268 margin: 0;
269 269 }
270 270 &:before,
271 271 &:after {
272 272 content: "";
273 273 clear: both;
274 274 }
275 275 }
276 276
277 277 // Gists
278 278 #files_data {
279 279 clear: both; //for firefox
280 padding-top: 10px;
280 281 }
282
281 283 #gistid {
282 284 margin-right: @padding;
283 285 }
284 286
285 287 // Global Settings Editor
286 288 .textarea.editor {
287 289 float: left;
288 290 position: relative;
289 291 max-width: @texteditor-width;
290 292
291 293 select {
292 294 position: absolute;
293 295 top:10px;
294 296 right:0;
295 297 }
296 298
297 299 .CodeMirror {
298 300 margin: 0;
299 301 }
300 302
301 303 .help-block {
302 304 margin: 0 0 @padding;
303 305 padding:.5em;
304 306 background-color: @grey6;
305 307 &.pre-formatting {
306 308 white-space: pre;
307 309 }
308 310 }
309 311 }
310 312
311 313 ul.auth_plugins {
312 314 margin: @padding 0 @padding @legend-width;
313 315 padding: 0;
314 316
315 317 li {
316 318 margin-bottom: @padding;
317 319 line-height: 1em;
318 320 list-style-type: none;
319 321
320 322 .auth_buttons .btn {
321 323 margin-right: @padding;
322 324 }
323 325
324 326 }
325 327 }
326 328
327 329
328 330 // My Account PR list
329 331
330 332 #show_closed {
331 333 margin: 0 1em 0 0;
332 334 }
333 335
334 336 .pullrequestlist {
335 337 .closed {
336 338 background-color: @grey6;
337 339 }
338 340 .td-status {
339 341 padding-left: .5em;
340 342 }
341 343 .log-container .truncate {
342 344 height: 2.75em;
343 345 white-space: pre-line;
344 346 }
345 347 table.rctable .user {
346 348 padding-left: 0;
347 349 }
348 350 table.rctable {
349 351 td.td-description,
350 352 .rc-user {
351 353 min-width: auto;
352 354 }
353 355 }
354 356 }
355 357
356 358 // Pull Requests
357 359
358 360 .pullrequests_section_head {
359 361 display: block;
360 362 clear: both;
361 363 margin: @padding 0;
362 364 font-weight: @text-bold-weight;
363 365 font-family: @text-bold;
364 366 }
365 367
366 368 .pr-origininfo, .pr-targetinfo {
367 369 position: relative;
368 370
369 371 .tag {
370 372 display: inline-block;
371 373 margin: 0 1em .5em 0;
372 374 }
373 375
374 376 .clone-url {
375 377 display: inline-block;
376 378 margin: 0 0 .5em 0;
377 379 padding: 0;
378 380 line-height: 1.2em;
379 381 }
380 382 }
381 383
382 384 .pr-mergeinfo {
383 385 min-width: 95% !important;
384 386 padding: 0 !important;
385 387 border: 0;
386 388 }
387 389 .pr-mergeinfo-copy {
388 390 padding: 0 0;
389 391 }
390 392
391 393 .pr-pullinfo {
392 394 min-width: 95% !important;
393 395 padding: 0 !important;
394 396 border: 0;
395 397 }
396 398 .pr-pullinfo-copy {
397 399 padding: 0 0;
398 400 }
399 401
400 402
401 403 #pr-title-input {
402 404 width: 72%;
403 405 font-size: 1em;
404 406 margin: 0;
405 407 padding: 0 0 0 @padding/4;
406 408 line-height: 1.7em;
407 409 color: @text-color;
408 410 letter-spacing: .02em;
409 411 font-weight: @text-bold-weight;
410 412 font-family: @text-bold;
411 413 }
412 414
413 415 #pullrequest_title {
414 416 width: 100%;
415 417 box-sizing: border-box;
416 418 }
417 419
418 420 #pr_open_message {
419 421 border: @border-thickness solid #fff;
420 422 border-radius: @border-radius;
421 423 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
422 424 text-align: left;
423 425 overflow: hidden;
424 426 }
425 427
426 428 .pr-submit-button {
427 429 float: right;
428 430 margin: 0 0 0 5px;
429 431 }
430 432
431 433 .pr-spacing-container {
432 434 padding: 20px;
433 435 clear: both
434 436 }
435 437
436 438 #pr-description-input {
437 439 margin-bottom: 0;
438 440 }
439 441
440 442 .pr-description-label {
441 443 vertical-align: top;
442 444 }
443 445
444 446 .perms_section_head {
445 447 min-width: 625px;
446 448
447 449 h2 {
448 450 margin-bottom: 0;
449 451 }
450 452
451 453 .label-checkbox {
452 454 float: left;
453 455 }
454 456
455 457 &.field {
456 458 margin: @space 0 @padding;
457 459 }
458 460
459 461 &:first-child.field {
460 462 margin-top: 0;
461 463
462 464 .label {
463 465 margin-top: 0;
464 466 padding-top: 0;
465 467 }
466 468
467 469 .radios {
468 470 padding-top: 0;
469 471 }
470 472 }
471 473
472 474 .radios {
473 475 position: relative;
474 476 width: 505px;
475 477 }
476 478 }
477 479
478 480 //--- MODULES ------------------//
479 481
480 482
481 483 // Server Announcement
482 484 #server-announcement {
483 485 width: 95%;
484 486 margin: @padding auto;
485 487 padding: @padding;
486 488 border-width: 2px;
487 489 border-style: solid;
488 490 .border-radius(2px);
489 491 font-weight: @text-bold-weight;
490 492 font-family: @text-bold;
491 493
492 494 &.info { border-color: @alert4; background-color: @alert4-inner; }
493 495 &.warning { border-color: @alert3; background-color: @alert3-inner; }
494 496 &.error { border-color: @alert2; background-color: @alert2-inner; }
495 497 &.success { border-color: @alert1; background-color: @alert1-inner; }
496 498 &.neutral { border-color: @grey3; background-color: @grey6; }
497 499 }
498 500
499 501 // Fixed Sidebar Column
500 502 .sidebar-col-wrapper {
501 503 padding-left: @sidebar-all-width;
502 504
503 505 .sidebar {
504 506 width: @sidebar-width;
505 507 margin-left: -@sidebar-all-width;
506 508 }
507 509 }
508 510
509 511 .sidebar-col-wrapper.scw-small {
510 512 padding-left: @sidebar-small-all-width;
511 513
512 514 .sidebar {
513 515 width: @sidebar-small-width;
514 516 margin-left: -@sidebar-small-all-width;
515 517 }
516 518 }
517 519
518 520
519 521 // FOOTER
520 522 #footer {
521 523 padding: 0;
522 524 text-align: center;
523 525 vertical-align: middle;
524 526 color: @grey2;
525 527 font-size: 11px;
526 528
527 529 p {
528 530 margin: 0;
529 531 padding: 1em;
530 532 line-height: 1em;
531 533 }
532 534
533 535 .server-instance { //server instance
534 536 display: none;
535 537 }
536 538
537 539 .title {
538 540 float: none;
539 541 margin: 0 auto;
540 542 }
541 543 }
542 544
543 545 button.close {
544 546 padding: 0;
545 547 cursor: pointer;
546 548 background: transparent;
547 549 border: 0;
548 550 .box-shadow(none);
549 551 -webkit-appearance: none;
550 552 }
551 553
552 554 .close {
553 555 float: right;
554 556 font-size: 21px;
555 557 font-family: @text-bootstrap;
556 558 line-height: 1em;
557 559 font-weight: bold;
558 560 color: @grey2;
559 561
560 562 &:hover,
561 563 &:focus {
562 564 color: @grey1;
563 565 text-decoration: none;
564 566 cursor: pointer;
565 567 }
566 568 }
567 569
568 570 // GRID
569 571 .sorting,
570 572 .sorting_desc,
571 573 .sorting_asc {
572 574 cursor: pointer;
573 575 }
574 576 .sorting_desc:after {
575 577 content: "\00A0\25B2";
576 578 font-size: .75em;
577 579 }
578 580 .sorting_asc:after {
579 581 content: "\00A0\25BC";
580 582 font-size: .68em;
581 583 }
582 584
583 585
584 586 .user_auth_tokens {
585 587
586 588 &.truncate {
587 589 white-space: nowrap;
588 590 overflow: hidden;
589 591 text-overflow: ellipsis;
590 592 }
591 593
592 594 .fields .field .input {
593 595 margin: 0;
594 596 }
595 597
596 598 input#description {
597 599 width: 100px;
598 600 margin: 0;
599 601 }
600 602
601 603 .drop-menu {
602 604 // TODO: johbo: Remove this, should work out of the box when
603 605 // having multiple inputs inline
604 606 margin: 0 0 0 5px;
605 607 }
606 608 }
607 609 #user_list_table {
608 610 .closed {
609 611 background-color: @grey6;
610 612 }
611 613 }
612 614
613 615
614 616 input, textarea {
615 617 &.disabled {
616 618 opacity: .5;
617 619 }
618 620
619 621 &:hover {
620 622 border-color: @grey3;
621 623 box-shadow: @button-shadow;
622 624 }
623 625
624 626 &:focus {
625 627 border-color: @rcblue;
626 628 box-shadow: @button-shadow;
627 629 }
628 630 }
629 631
630 632 // remove extra padding in firefox
631 633 input::-moz-focus-inner { border:0; padding:0 }
632 634
633 635 .adjacent input {
634 636 margin-bottom: @padding;
635 637 }
636 638
637 639 .permissions_boxes {
638 640 display: block;
639 641 }
640 642
641 643 //FORMS
642 644
643 645 .medium-inline,
644 646 input#description.medium-inline {
645 647 display: inline;
646 648 width: @medium-inline-input-width;
647 649 min-width: 100px;
648 650 }
649 651
650 652 select {
651 653 //reset
652 654 -webkit-appearance: none;
653 655 -moz-appearance: none;
654 656
655 657 display: inline-block;
656 658 height: 28px;
657 659 width: auto;
658 660 margin: 0 @padding @padding 0;
659 661 padding: 0 18px 0 8px;
660 662 line-height:1em;
661 663 font-size: @basefontsize;
662 664 border: @border-thickness solid @grey5;
663 665 border-radius: @border-radius;
664 666 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
665 667 color: @grey4;
666 668 box-shadow: @button-shadow;
667 669
668 670 &:after {
669 671 content: "\00A0\25BE";
670 672 }
671 673
672 674 &:focus, &:hover {
673 675 outline: none;
674 676 border-color: @grey4;
675 677 color: @rcdarkblue;
676 678 }
677 679 }
678 680
679 681 option {
680 682 &:focus {
681 683 outline: none;
682 684 }
683 685 }
684 686
685 687 input,
686 688 textarea {
687 689 padding: @input-padding;
688 690 border: @input-border-thickness solid @border-highlight-color;
689 691 .border-radius (@border-radius);
690 692 font-family: @text-light;
691 693 font-size: @basefontsize;
692 694
693 695 &.input-sm {
694 696 padding: 5px;
695 697 }
696 698
697 699 &#description {
698 700 min-width: @input-description-minwidth;
699 701 min-height: 1em;
700 702 padding: 10px;
701 703 }
702 704 }
703 705
704 706 .field-sm {
705 707 input,
706 708 textarea {
707 709 padding: 5px;
708 710 }
709 711 }
710 712
711 713 textarea {
712 714 display: block;
713 715 clear: both;
714 716 width: 100%;
715 717 min-height: 100px;
716 718 margin-bottom: @padding;
717 719 .box-sizing(border-box);
718 720 overflow: auto;
719 721 }
720 722
721 723 label {
722 724 font-family: @text-light;
723 725 }
724 726
725 727 // GRAVATARS
726 728 // centers gravatar on username to the right
727 729
728 730 .gravatar {
729 731 display: inline;
730 732 min-width: 16px;
731 733 min-height: 16px;
732 734 margin: -5px 0;
733 735 padding: 0;
734 736 line-height: 1em;
735 737 box-sizing: content-box;
736 738 border-radius: 50%;
737 739
738 740 &.gravatar-large {
739 741 margin: -0.5em .25em -0.5em 0;
740 742 }
741 743
742 744 & + .user {
743 745 display: inline;
744 746 margin: 0;
745 747 padding: 0 0 0 .17em;
746 748 line-height: 1em;
747 749 }
748 750 }
749 751
750 752 .user-inline-data {
751 753 display: inline-block;
752 754 float: left;
753 755 padding-left: .5em;
754 756 line-height: 1.3em;
755 757 }
756 758
757 759 .rc-user { // gravatar + user wrapper
758 760 float: left;
759 761 position: relative;
760 762 min-width: 100px;
761 763 max-width: 200px;
762 764 min-height: (@gravatar-size + @border-thickness * 2); // account for border
763 765 display: block;
764 766 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
765 767
766 768
767 769 .gravatar {
768 770 display: block;
769 771 position: absolute;
770 772 top: 0;
771 773 left: 0;
772 774 min-width: @gravatar-size;
773 775 min-height: @gravatar-size;
774 776 margin: 0;
775 777 }
776 778
777 779 .user {
778 780 display: block;
779 781 max-width: 175px;
780 782 padding-top: 2px;
781 783 overflow: hidden;
782 784 text-overflow: ellipsis;
783 785 }
784 786 }
785 787
786 788 .gist-gravatar,
787 789 .journal_container {
788 790 .gravatar-large {
789 791 margin: 0 .5em -10px 0;
790 792 }
791 793 }
792 794
793 795
794 796 // ADMIN SETTINGS
795 797
796 798 // Tag Patterns
797 799 .tag_patterns {
798 800 .tag_input {
799 801 margin-bottom: @padding;
800 802 }
801 803 }
802 804
803 805 .locked_input {
804 806 position: relative;
805 807
806 808 input {
807 809 display: inline;
808 810 margin: 3px 5px 0px 0px;
809 811 }
810 812
811 813 br {
812 814 display: none;
813 815 }
814 816
815 817 .error-message {
816 818 float: left;
817 819 width: 100%;
818 820 }
819 821
820 822 .lock_input_button {
821 823 display: inline;
822 824 }
823 825
824 826 .help-block {
825 827 clear: both;
826 828 }
827 829 }
828 830
829 831 // Notifications
830 832
831 833 .notifications_buttons {
832 834 margin: 0 0 @space 0;
833 835 padding: 0;
834 836
835 837 .btn {
836 838 display: inline-block;
837 839 }
838 840 }
839 841
840 842 .notification-list {
841 843
842 844 div {
843 845 display: inline-block;
844 846 vertical-align: middle;
845 847 }
846 848
847 849 .container {
848 850 display: block;
849 851 margin: 0 0 @padding 0;
850 852 }
851 853
852 854 .delete-notifications {
853 855 margin-left: @padding;
854 856 text-align: right;
855 857 cursor: pointer;
856 858 }
857 859
858 860 .read-notifications {
859 861 margin-left: @padding/2;
860 862 text-align: right;
861 863 width: 35px;
862 864 cursor: pointer;
863 865 }
864 866
865 867 .icon-minus-sign {
866 868 color: @alert2;
867 869 }
868 870
869 871 .icon-ok-sign {
870 872 color: @alert1;
871 873 }
872 874 }
873 875
874 876 .user_settings {
875 877 float: left;
876 878 clear: both;
877 879 display: block;
878 880 width: 100%;
879 881
880 882 .gravatar_box {
881 883 margin-bottom: @padding;
882 884
883 885 &:after {
884 886 content: " ";
885 887 clear: both;
886 888 width: 100%;
887 889 }
888 890 }
889 891
890 892 .fields .field {
891 893 clear: both;
892 894 }
893 895 }
894 896
895 897 .advanced_settings {
896 898 margin-bottom: @space;
897 899
898 900 .help-block {
899 901 margin-left: 0;
900 902 }
901 903
902 904 button + .help-block {
903 905 margin-top: @padding;
904 906 }
905 907 }
906 908
907 909 // admin settings radio buttons and labels
908 910 .label-2 {
909 911 float: left;
910 912 width: @label2-width;
911 913
912 914 label {
913 915 color: @grey1;
914 916 }
915 917 }
916 918 .checkboxes {
917 919 float: left;
918 920 width: @checkboxes-width;
919 921 margin-bottom: @padding;
920 922
921 923 .checkbox {
922 924 width: 100%;
923 925
924 926 label {
925 927 margin: 0;
926 928 padding: 0;
927 929 }
928 930 }
929 931
930 932 .checkbox + .checkbox {
931 933 display: inline-block;
932 934 }
933 935
934 936 label {
935 937 margin-right: 1em;
936 938 }
937 939 }
938 940
939 941 // CHANGELOG
940 942 .container_header {
941 943 float: left;
942 944 display: block;
943 945 width: 100%;
944 946 margin: @padding 0 @padding;
945 947
946 948 #filter_changelog {
947 949 float: left;
948 950 margin-right: @padding;
949 951 }
950 952
951 953 .breadcrumbs_light {
952 954 display: inline-block;
953 955 }
954 956 }
955 957
956 958 .info_box {
957 959 float: right;
958 960 }
959 961
960 962
961 963 #graph_nodes {
962 964 padding-top: 43px;
963 965 }
964 966
965 967 #graph_content{
966 968
967 969 // adjust for table headers so that graph renders properly
968 970 // #graph_nodes padding - table cell padding
969 971 padding-top: (@space - (@basefontsize * 2.4));
970 972
971 973 &.graph_full_width {
972 974 width: 100%;
973 975 max-width: 100%;
974 976 }
975 977 }
976 978
977 979 #graph {
978 980 .flag_status {
979 981 margin: 0;
980 982 }
981 983
982 984 .pagination-left {
983 985 float: left;
984 986 clear: both;
985 987 }
986 988
987 989 .log-container {
988 990 max-width: 345px;
989 991
990 992 .message{
991 993 max-width: 340px;
992 994 }
993 995 }
994 996
995 997 .graph-col-wrapper {
996 998 padding-left: 110px;
997 999
998 1000 #graph_nodes {
999 1001 width: 100px;
1000 1002 margin-left: -110px;
1001 1003 float: left;
1002 1004 clear: left;
1003 1005 }
1004 1006 }
1005 1007
1006 1008 .load-more-commits {
1007 1009 text-align: center;
1008 1010 }
1009 1011 .load-more-commits:hover {
1010 1012 background-color: @grey7;
1011 1013 }
1012 1014 .load-more-commits {
1013 1015 a {
1014 1016 display: block;
1015 1017 }
1016 1018 }
1017 1019 }
1018 1020
1019 1021 #filter_changelog {
1020 1022 float: left;
1021 1023 }
1022 1024
1023 1025
1024 1026 //--- THEME ------------------//
1025 1027
1026 1028 #logo {
1027 1029 float: left;
1028 1030 margin: 9px 0 0 0;
1029 1031
1030 1032 .header {
1031 1033 background-color: transparent;
1032 1034 }
1033 1035
1034 1036 a {
1035 1037 display: inline-block;
1036 1038 }
1037 1039
1038 1040 img {
1039 1041 height:30px;
1040 1042 }
1041 1043 }
1042 1044
1043 1045 .logo-wrapper {
1044 1046 float:left;
1045 1047 }
1046 1048
1047 1049 .branding {
1048 1050 float: left;
1049 1051 padding: 9px 2px;
1050 1052 line-height: 1em;
1051 1053 font-size: @navigation-fontsize;
1052 1054
1053 1055 a {
1054 1056 color: @grey5
1055 1057 }
1056 1058 }
1057 1059
1058 1060 img {
1059 1061 border: none;
1060 1062 outline: none;
1061 1063 }
1062 1064 user-profile-header
1063 1065 label {
1064 1066
1065 1067 input[type="checkbox"] {
1066 1068 margin-right: 1em;
1067 1069 }
1068 1070 input[type="radio"] {
1069 1071 margin-right: 1em;
1070 1072 }
1071 1073 }
1072 1074
1073 1075 .flag_status {
1074 1076 margin: 2px;
1075 1077 &.under_review {
1076 1078 .circle(5px, @alert3);
1077 1079 }
1078 1080 &.approved {
1079 1081 .circle(5px, @alert1);
1080 1082 }
1081 1083 &.rejected,
1082 1084 &.forced_closed{
1083 1085 .circle(5px, @alert2);
1084 1086 }
1085 1087 &.not_reviewed {
1086 1088 .circle(5px, @grey5);
1087 1089 }
1088 1090 }
1089 1091
1090 1092 .flag_status_comment_box {
1091 1093 margin: 5px 6px 0px 2px;
1092 1094 }
1093 1095 .test_pattern_preview {
1094 1096 margin: @space 0;
1095 1097
1096 1098 p {
1097 1099 margin-bottom: 0;
1098 1100 border-bottom: @border-thickness solid @border-default-color;
1099 1101 color: @grey3;
1100 1102 }
1101 1103
1102 1104 .btn {
1103 1105 margin-bottom: @padding;
1104 1106 }
1105 1107 }
1106 1108 #test_pattern_result {
1107 1109 display: none;
1108 1110 &:extend(pre);
1109 1111 padding: .9em;
1110 1112 color: @grey3;
1111 1113 background-color: @grey7;
1112 1114 border-right: @border-thickness solid @border-default-color;
1113 1115 border-bottom: @border-thickness solid @border-default-color;
1114 1116 border-left: @border-thickness solid @border-default-color;
1115 1117 }
1116 1118
1117 1119 #repo_vcs_settings {
1118 1120 #inherit_overlay_vcs_default {
1119 1121 display: none;
1120 1122 }
1121 1123 #inherit_overlay_vcs_custom {
1122 1124 display: custom;
1123 1125 }
1124 1126 &.inherited {
1125 1127 #inherit_overlay_vcs_default {
1126 1128 display: block;
1127 1129 }
1128 1130 #inherit_overlay_vcs_custom {
1129 1131 display: none;
1130 1132 }
1131 1133 }
1132 1134 }
1133 1135
1134 1136 .issue-tracker-link {
1135 1137 color: @rcblue;
1136 1138 }
1137 1139
1138 1140 // Issue Tracker Table Show/Hide
1139 1141 #repo_issue_tracker {
1140 1142 #inherit_overlay {
1141 1143 display: none;
1142 1144 }
1143 1145 #custom_overlay {
1144 1146 display: custom;
1145 1147 }
1146 1148 &.inherited {
1147 1149 #inherit_overlay {
1148 1150 display: block;
1149 1151 }
1150 1152 #custom_overlay {
1151 1153 display: none;
1152 1154 }
1153 1155 }
1154 1156 }
1155 1157 table.issuetracker {
1156 1158 &.readonly {
1157 1159 tr, td {
1158 1160 color: @grey3;
1159 1161 }
1160 1162 }
1161 1163 .edit {
1162 1164 display: none;
1163 1165 }
1164 1166 .editopen {
1165 1167 .edit {
1166 1168 display: inline;
1167 1169 }
1168 1170 .entry {
1169 1171 display: none;
1170 1172 }
1171 1173 }
1172 1174 tr td.td-action {
1173 1175 min-width: 117px;
1174 1176 }
1175 1177 td input {
1176 1178 max-width: none;
1177 1179 min-width: 30px;
1178 1180 width: 80%;
1179 1181 }
1180 1182 .issuetracker_pref input {
1181 1183 width: 40%;
1182 1184 }
1183 1185 input.edit_issuetracker_update {
1184 1186 margin-right: 0;
1185 1187 width: auto;
1186 1188 }
1187 1189 }
1188 1190
1189 1191 table.integrations {
1190 1192 .td-icon {
1191 1193 width: 20px;
1192 1194 .integration-icon {
1193 1195 height: 20px;
1194 1196 width: 20px;
1195 1197 }
1196 1198 }
1197 1199 }
1198 1200
1199 1201 .integrations {
1200 1202 a.integration-box {
1201 1203 color: @text-color;
1202 1204 &:hover {
1203 1205 .panel {
1204 1206 background: #fbfbfb;
1205 1207 }
1206 1208 }
1207 1209 .integration-icon {
1208 1210 width: 30px;
1209 1211 height: 30px;
1210 1212 margin-right: 20px;
1211 1213 float: left;
1212 1214 }
1213 1215
1214 1216 .panel-body {
1215 1217 padding: 10px;
1216 1218 }
1217 1219 .panel {
1218 1220 margin-bottom: 10px;
1219 1221 }
1220 1222 h2 {
1221 1223 display: inline-block;
1222 1224 margin: 0;
1223 1225 min-width: 140px;
1224 1226 }
1225 1227 }
1226 1228 a.integration-box.dummy-integration {
1227 1229 color: @grey4
1228 1230 }
1229 1231 }
1230 1232
1231 1233 //Permissions Settings
1232 1234 #add_perm {
1233 1235 margin: 0 0 @padding;
1234 1236 cursor: pointer;
1235 1237 }
1236 1238
1237 1239 .perm_ac {
1238 1240 input {
1239 1241 width: 95%;
1240 1242 }
1241 1243 }
1242 1244
1243 1245 .autocomplete-suggestions {
1244 1246 width: auto !important; // overrides autocomplete.js
1245 1247 min-width: 278px;
1246 1248 margin: 0;
1247 1249 border: @border-thickness solid @grey5;
1248 1250 border-radius: @border-radius;
1249 1251 color: @grey2;
1250 1252 background-color: white;
1251 1253 }
1252 1254
1253 1255 .autocomplete-qfilter-suggestions {
1254 1256 width: auto !important; // overrides autocomplete.js
1255 1257 max-height: 100% !important;
1256 1258 min-width: 376px;
1257 1259 margin: 0;
1258 1260 border: @border-thickness solid @grey5;
1259 1261 color: @grey2;
1260 1262 background-color: white;
1261 1263 }
1262 1264
1263 1265 .autocomplete-selected {
1264 1266 background: #F0F0F0;
1265 1267 }
1266 1268
1267 1269 .ac-container-wrap {
1268 1270 margin: 0;
1269 1271 padding: 8px;
1270 1272 border-bottom: @border-thickness solid @grey5;
1271 1273 list-style-type: none;
1272 1274 cursor: pointer;
1273 1275
1274 1276 &:hover {
1275 1277 background-color: @grey7;
1276 1278 }
1277 1279
1278 1280 img {
1279 1281 height: @gravatar-size;
1280 1282 width: @gravatar-size;
1281 1283 margin-right: 1em;
1282 1284 }
1283 1285
1284 1286 strong {
1285 1287 font-weight: normal;
1286 1288 }
1287 1289 }
1288 1290
1289 1291 // Settings Dropdown
1290 1292 .user-menu .container {
1291 1293 padding: 0 4px;
1292 1294 margin: 0;
1293 1295 }
1294 1296
1295 1297 .user-menu .gravatar {
1296 1298 cursor: pointer;
1297 1299 }
1298 1300
1299 1301 .codeblock {
1300 1302 margin-bottom: @padding;
1301 1303 clear: both;
1302 1304
1303 1305 .stats {
1304 1306 overflow: hidden;
1305 1307 }
1306 1308
1307 1309 .message{
1308 1310 textarea{
1309 1311 margin: 0;
1310 1312 }
1311 1313 }
1312 1314
1313 1315 .code-header {
1314 1316 .stats {
1315 1317 line-height: 2em;
1316 1318
1317 1319 .revision_id {
1318 1320 margin-left: 0;
1319 1321 }
1320 1322 .buttons {
1321 1323 padding-right: 0;
1322 1324 }
1323 1325 }
1324 1326
1325 1327 .item{
1326 1328 margin-right: 0.5em;
1327 1329 }
1328 1330 }
1329 1331
1330 #editor_container{
1331 position: relative;
1332 margin: @padding;
1332 #editor_container {
1333 position: relative;
1334 margin: @padding 10px;
1333 1335 }
1334 1336 }
1335 1337
1336 1338 #file_history_container {
1337 1339 display: none;
1338 1340 }
1339 1341
1340 1342 .file-history-inner {
1341 1343 margin-bottom: 10px;
1342 1344 }
1343 1345
1344 1346 // Pull Requests
1345 1347 .summary-details {
1346 1348 width: 72%;
1347 1349 }
1348 1350 .pr-summary {
1349 1351 border-bottom: @border-thickness solid @grey5;
1350 1352 margin-bottom: @space;
1351 1353 }
1352 1354 .reviewers-title {
1353 1355 width: 25%;
1354 1356 min-width: 200px;
1355 1357 }
1356 1358 .reviewers {
1357 1359 width: 25%;
1358 1360 min-width: 200px;
1359 1361 }
1360 1362 .reviewers ul li {
1361 1363 position: relative;
1362 1364 width: 100%;
1363 1365 padding-bottom: 8px;
1364 1366 list-style-type: none;
1365 1367 }
1366 1368
1367 1369 .reviewer_entry {
1368 1370 min-height: 55px;
1369 1371 }
1370 1372
1371 1373 .reviewers_member {
1372 1374 width: 100%;
1373 1375 overflow: auto;
1374 1376 }
1375 1377 .reviewer_reason {
1376 1378 padding-left: 20px;
1377 1379 line-height: 1.5em;
1378 1380 }
1379 1381 .reviewer_status {
1380 1382 display: inline-block;
1381 1383 vertical-align: top;
1382 1384 width: 25px;
1383 1385 min-width: 25px;
1384 1386 height: 1.2em;
1385 1387 margin-top: 3px;
1386 1388 line-height: 1em;
1387 1389 }
1388 1390
1389 1391 .reviewer_name {
1390 1392 display: inline-block;
1391 1393 max-width: 83%;
1392 1394 padding-right: 20px;
1393 1395 vertical-align: middle;
1394 1396 line-height: 1;
1395 1397
1396 1398 .rc-user {
1397 1399 min-width: 0;
1398 1400 margin: -2px 1em 0 0;
1399 1401 }
1400 1402
1401 1403 .reviewer {
1402 1404 float: left;
1403 1405 }
1404 1406 }
1405 1407
1406 1408 .reviewer_member_mandatory {
1407 1409 position: absolute;
1408 1410 left: 15px;
1409 1411 top: 8px;
1410 1412 width: 16px;
1411 1413 font-size: 11px;
1412 1414 margin: 0;
1413 1415 padding: 0;
1414 1416 color: black;
1415 1417 }
1416 1418
1417 1419 .reviewer_member_mandatory_remove,
1418 1420 .reviewer_member_remove {
1419 1421 position: absolute;
1420 1422 right: 0;
1421 1423 top: 0;
1422 1424 width: 16px;
1423 1425 margin-bottom: 10px;
1424 1426 padding: 0;
1425 1427 color: black;
1426 1428 }
1427 1429
1428 1430 .reviewer_member_mandatory_remove {
1429 1431 color: @grey4;
1430 1432 }
1431 1433
1432 1434 .reviewer_member_status {
1433 1435 margin-top: 5px;
1434 1436 }
1435 1437 .pr-summary #summary{
1436 1438 width: 100%;
1437 1439 }
1438 1440 .pr-summary .action_button:hover {
1439 1441 border: 0;
1440 1442 cursor: pointer;
1441 1443 }
1442 1444 .pr-details-title {
1443 1445 padding-bottom: 8px;
1444 1446 border-bottom: @border-thickness solid @grey5;
1445 1447
1446 1448 .action_button.disabled {
1447 1449 color: @grey4;
1448 1450 cursor: inherit;
1449 1451 }
1450 1452 .action_button {
1451 1453 color: @rcblue;
1452 1454 }
1453 1455 }
1454 1456 .pr-details-content {
1455 1457 margin-top: @textmargin;
1456 1458 margin-bottom: @textmargin;
1457 1459 }
1458 1460
1459 1461 .pr-reviewer-rules {
1460 1462 padding: 10px 0px 20px 0px;
1461 1463 }
1462 1464
1463 1465 .group_members {
1464 1466 margin-top: 0;
1465 1467 padding: 0;
1466 1468 list-style: outside none none;
1467 1469
1468 1470 img {
1469 1471 height: @gravatar-size;
1470 1472 width: @gravatar-size;
1471 1473 margin-right: .5em;
1472 1474 margin-left: 3px;
1473 1475 }
1474 1476
1475 1477 .to-delete {
1476 1478 .user {
1477 1479 text-decoration: line-through;
1478 1480 }
1479 1481 }
1480 1482 }
1481 1483
1482 1484 .compare_view_commits_title {
1483 1485 .disabled {
1484 1486 cursor: inherit;
1485 1487 &:hover{
1486 1488 background-color: inherit;
1487 1489 color: inherit;
1488 1490 }
1489 1491 }
1490 1492 }
1491 1493
1492 1494 .subtitle-compare {
1493 1495 margin: -15px 0px 0px 0px;
1494 1496 }
1495 1497
1496 1498 .comments-summary-td {
1497 1499 border-top: 1px dashed @grey5;
1498 1500 }
1499 1501
1500 1502 // new entry in group_members
1501 1503 .td-author-new-entry {
1502 1504 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1503 1505 }
1504 1506
1505 1507 .usergroup_member_remove {
1506 1508 width: 16px;
1507 1509 margin-bottom: 10px;
1508 1510 padding: 0;
1509 1511 color: black !important;
1510 1512 cursor: pointer;
1511 1513 }
1512 1514
1513 1515 .reviewer_ac .ac-input {
1514 1516 width: 92%;
1515 1517 margin-bottom: 1em;
1516 1518 }
1517 1519
1518 1520 .compare_view_commits tr{
1519 1521 height: 20px;
1520 1522 }
1521 1523 .compare_view_commits td {
1522 1524 vertical-align: top;
1523 1525 padding-top: 10px;
1524 1526 }
1525 1527 .compare_view_commits .author {
1526 1528 margin-left: 5px;
1527 1529 }
1528 1530
1529 1531 .compare_view_commits {
1530 1532 .color-a {
1531 1533 color: @alert1;
1532 1534 }
1533 1535
1534 1536 .color-c {
1535 1537 color: @color3;
1536 1538 }
1537 1539
1538 1540 .color-r {
1539 1541 color: @color5;
1540 1542 }
1541 1543
1542 1544 .color-a-bg {
1543 1545 background-color: @alert1;
1544 1546 }
1545 1547
1546 1548 .color-c-bg {
1547 1549 background-color: @alert3;
1548 1550 }
1549 1551
1550 1552 .color-r-bg {
1551 1553 background-color: @alert2;
1552 1554 }
1553 1555
1554 1556 .color-a-border {
1555 1557 border: 1px solid @alert1;
1556 1558 }
1557 1559
1558 1560 .color-c-border {
1559 1561 border: 1px solid @alert3;
1560 1562 }
1561 1563
1562 1564 .color-r-border {
1563 1565 border: 1px solid @alert2;
1564 1566 }
1565 1567
1566 1568 .commit-change-indicator {
1567 1569 width: 15px;
1568 1570 height: 15px;
1569 1571 position: relative;
1570 1572 left: 15px;
1571 1573 }
1572 1574
1573 1575 .commit-change-content {
1574 1576 text-align: center;
1575 1577 vertical-align: middle;
1576 1578 line-height: 15px;
1577 1579 }
1578 1580 }
1579 1581
1580 1582 .compare_view_filepath {
1581 1583 color: @grey1;
1582 1584 }
1583 1585
1584 1586 .show_more {
1585 1587 display: inline-block;
1586 1588 width: 0;
1587 1589 height: 0;
1588 1590 vertical-align: middle;
1589 1591 content: "";
1590 1592 border: 4px solid;
1591 1593 border-right-color: transparent;
1592 1594 border-bottom-color: transparent;
1593 1595 border-left-color: transparent;
1594 1596 font-size: 0;
1595 1597 }
1596 1598
1597 1599 .journal_more .show_more {
1598 1600 display: inline;
1599 1601
1600 1602 &:after {
1601 1603 content: none;
1602 1604 }
1603 1605 }
1604 1606
1605 1607 .compare_view_commits .collapse_commit:after {
1606 1608 cursor: pointer;
1607 1609 content: "\00A0\25B4";
1608 1610 margin-left: -3px;
1609 1611 font-size: 17px;
1610 1612 color: @grey4;
1611 1613 }
1612 1614
1613 1615 .diff_links {
1614 1616 margin-left: 8px;
1615 1617 }
1616 1618
1617 1619 div.ancestor {
1618 1620 margin: -30px 0px;
1619 1621 }
1620 1622
1621 1623 .cs_icon_td input[type="checkbox"] {
1622 1624 display: none;
1623 1625 }
1624 1626
1625 1627 .cs_icon_td .expand_file_icon:after {
1626 1628 cursor: pointer;
1627 1629 content: "\00A0\25B6";
1628 1630 font-size: 12px;
1629 1631 color: @grey4;
1630 1632 }
1631 1633
1632 1634 .cs_icon_td .collapse_file_icon:after {
1633 1635 cursor: pointer;
1634 1636 content: "\00A0\25BC";
1635 1637 font-size: 12px;
1636 1638 color: @grey4;
1637 1639 }
1638 1640
1639 1641 /*new binary
1640 1642 NEW_FILENODE = 1
1641 1643 DEL_FILENODE = 2
1642 1644 MOD_FILENODE = 3
1643 1645 RENAMED_FILENODE = 4
1644 1646 COPIED_FILENODE = 5
1645 1647 CHMOD_FILENODE = 6
1646 1648 BIN_FILENODE = 7
1647 1649 */
1648 1650 .cs_files_expand {
1649 1651 font-size: @basefontsize + 5px;
1650 1652 line-height: 1.8em;
1651 1653 float: right;
1652 1654 }
1653 1655
1654 1656 .cs_files_expand span{
1655 1657 color: @rcblue;
1656 1658 cursor: pointer;
1657 1659 }
1658 1660 .cs_files {
1659 1661 clear: both;
1660 1662 padding-bottom: @padding;
1661 1663
1662 1664 .cur_cs {
1663 1665 margin: 10px 2px;
1664 1666 font-weight: bold;
1665 1667 }
1666 1668
1667 1669 .node {
1668 1670 float: left;
1669 1671 }
1670 1672
1671 1673 .changes {
1672 1674 float: right;
1673 1675 color: white;
1674 1676 font-size: @basefontsize - 4px;
1675 1677 margin-top: 4px;
1676 1678 opacity: 0.6;
1677 1679 filter: Alpha(opacity=60); /* IE8 and earlier */
1678 1680
1679 1681 .added {
1680 1682 background-color: @alert1;
1681 1683 float: left;
1682 1684 text-align: center;
1683 1685 }
1684 1686
1685 1687 .deleted {
1686 1688 background-color: @alert2;
1687 1689 float: left;
1688 1690 text-align: center;
1689 1691 }
1690 1692
1691 1693 .bin {
1692 1694 background-color: @alert1;
1693 1695 text-align: center;
1694 1696 }
1695 1697
1696 1698 /*new binary*/
1697 1699 .bin.bin1 {
1698 1700 background-color: @alert1;
1699 1701 text-align: center;
1700 1702 }
1701 1703
1702 1704 /*deleted binary*/
1703 1705 .bin.bin2 {
1704 1706 background-color: @alert2;
1705 1707 text-align: center;
1706 1708 }
1707 1709
1708 1710 /*mod binary*/
1709 1711 .bin.bin3 {
1710 1712 background-color: @grey2;
1711 1713 text-align: center;
1712 1714 }
1713 1715
1714 1716 /*rename file*/
1715 1717 .bin.bin4 {
1716 1718 background-color: @alert4;
1717 1719 text-align: center;
1718 1720 }
1719 1721
1720 1722 /*copied file*/
1721 1723 .bin.bin5 {
1722 1724 background-color: @alert4;
1723 1725 text-align: center;
1724 1726 }
1725 1727
1726 1728 /*chmod file*/
1727 1729 .bin.bin6 {
1728 1730 background-color: @grey2;
1729 1731 text-align: center;
1730 1732 }
1731 1733 }
1732 1734 }
1733 1735
1734 1736 .cs_files .cs_added, .cs_files .cs_A,
1735 1737 .cs_files .cs_added, .cs_files .cs_M,
1736 1738 .cs_files .cs_added, .cs_files .cs_D {
1737 1739 height: 16px;
1738 1740 padding-right: 10px;
1739 1741 margin-top: 7px;
1740 1742 text-align: left;
1741 1743 }
1742 1744
1743 1745 .cs_icon_td {
1744 1746 min-width: 16px;
1745 1747 width: 16px;
1746 1748 }
1747 1749
1748 1750 .pull-request-merge {
1749 1751 border: 1px solid @grey5;
1750 1752 padding: 10px 0px 20px;
1751 1753 margin-top: 10px;
1752 1754 margin-bottom: 20px;
1753 1755 }
1754 1756
1755 1757 .pull-request-merge ul {
1756 1758 padding: 0px 0px;
1757 1759 }
1758 1760
1759 1761 .pull-request-merge li {
1760 1762 list-style-type: none;
1761 1763 }
1762 1764
1763 1765 .pull-request-merge .pull-request-wrap {
1764 1766 height: auto;
1765 1767 padding: 0px 0px;
1766 1768 text-align: right;
1767 1769 }
1768 1770
1769 1771 .pull-request-merge span {
1770 1772 margin-right: 5px;
1771 1773 }
1772 1774
1773 1775 .pull-request-merge-actions {
1774 1776 min-height: 30px;
1775 1777 padding: 0px 0px;
1776 1778 }
1777 1779
1778 1780 .pull-request-merge-info {
1779 1781 padding: 0px 5px 5px 0px;
1780 1782 }
1781 1783
1782 1784 .merge-status {
1783 1785 margin-right: 5px;
1784 1786 }
1785 1787
1786 1788 .merge-message {
1787 1789 font-size: 1.2em
1788 1790 }
1789 1791
1790 1792 .merge-message.success i,
1791 1793 .merge-icon.success i {
1792 1794 color:@alert1;
1793 1795 }
1794 1796
1795 1797 .merge-message.warning i,
1796 1798 .merge-icon.warning i {
1797 1799 color: @alert3;
1798 1800 }
1799 1801
1800 1802 .merge-message.error i,
1801 1803 .merge-icon.error i {
1802 1804 color:@alert2;
1803 1805 }
1804 1806
1805 1807 .pr-versions {
1806 1808 font-size: 1.1em;
1807 1809
1808 1810 table {
1809 1811 padding: 0px 5px;
1810 1812 }
1811 1813
1812 1814 td {
1813 1815 line-height: 15px;
1814 1816 }
1815 1817
1816 1818 .flag_status {
1817 1819 margin: 0;
1818 1820 }
1819 1821
1820 1822 .compare-radio-button {
1821 1823 position: relative;
1822 1824 top: -3px;
1823 1825 }
1824 1826 }
1825 1827
1826 1828
1827 1829 #close_pull_request {
1828 1830 margin-right: 0px;
1829 1831 }
1830 1832
1831 1833 .empty_data {
1832 1834 color: @grey4;
1833 1835 }
1834 1836
1835 1837 #changeset_compare_view_content {
1836 1838 clear: both;
1837 1839 width: 100%;
1838 1840 box-sizing: border-box;
1839 1841 .border-radius(@border-radius);
1840 1842
1841 1843 .help-block {
1842 1844 margin: @padding 0;
1843 1845 color: @text-color;
1844 1846 &.pre-formatting {
1845 1847 white-space: pre;
1846 1848 }
1847 1849 }
1848 1850
1849 1851 .empty_data {
1850 1852 margin: @padding 0;
1851 1853 }
1852 1854
1853 1855 .alert {
1854 1856 margin-bottom: @space;
1855 1857 }
1856 1858 }
1857 1859
1858 1860 .table_disp {
1859 1861 .status {
1860 1862 width: auto;
1861 1863
1862 1864 .flag_status {
1863 1865 float: left;
1864 1866 }
1865 1867 }
1866 1868 }
1867 1869
1868 1870
1869 1871 .creation_in_progress {
1870 1872 color: @grey4
1871 1873 }
1872 1874
1873 1875 .status_box_menu {
1874 1876 margin: 0;
1875 1877 }
1876 1878
1877 1879 .notification-table{
1878 1880 margin-bottom: @space;
1879 1881 display: table;
1880 1882 width: 100%;
1881 1883
1882 1884 .container{
1883 1885 display: table-row;
1884 1886
1885 1887 .notification-header{
1886 1888 border-bottom: @border-thickness solid @border-default-color;
1887 1889 }
1888 1890
1889 1891 .notification-subject{
1890 1892 display: table-cell;
1891 1893 }
1892 1894 }
1893 1895 }
1894 1896
1895 1897 // Notifications
1896 1898 .notification-header{
1897 1899 display: table;
1898 1900 width: 100%;
1899 1901 padding: floor(@basefontsize/2) 0;
1900 1902 line-height: 1em;
1901 1903
1902 1904 .desc, .delete-notifications, .read-notifications{
1903 1905 display: table-cell;
1904 1906 text-align: left;
1905 1907 }
1906 1908
1907 1909 .desc{
1908 1910 width: 1163px;
1909 1911 }
1910 1912
1911 1913 .delete-notifications, .read-notifications{
1912 1914 width: 35px;
1913 1915 min-width: 35px; //fixes when only one button is displayed
1914 1916 }
1915 1917 }
1916 1918
1917 1919 .notification-body {
1918 1920 .markdown-block,
1919 1921 .rst-block {
1920 1922 padding: @padding 0;
1921 1923 }
1922 1924
1923 1925 .notification-subject {
1924 1926 padding: @textmargin 0;
1925 1927 border-bottom: @border-thickness solid @border-default-color;
1926 1928 }
1927 1929 }
1928 1930
1929 1931
1930 1932 .notifications_buttons{
1931 1933 float: right;
1932 1934 }
1933 1935
1934 1936 #notification-status{
1935 1937 display: inline;
1936 1938 }
1937 1939
1938 1940 // Repositories
1939 1941
1940 1942 #summary.fields{
1941 1943 display: table;
1942 1944
1943 1945 .field{
1944 1946 display: table-row;
1945 1947
1946 1948 .label-summary{
1947 1949 display: table-cell;
1948 1950 min-width: @label-summary-minwidth;
1949 1951 padding-top: @padding/2;
1950 1952 padding-bottom: @padding/2;
1951 1953 padding-right: @padding/2;
1952 1954 }
1953 1955
1954 1956 .input{
1955 1957 display: table-cell;
1956 1958 padding: @padding/2;
1957 1959
1958 1960 input{
1959 1961 min-width: 29em;
1960 1962 padding: @padding/4;
1961 1963 }
1962 1964 }
1963 1965 .statistics, .downloads{
1964 1966 .disabled{
1965 1967 color: @grey4;
1966 1968 }
1967 1969 }
1968 1970 }
1969 1971 }
1970 1972
1971 1973 #summary{
1972 1974 width: 70%;
1973 1975 }
1974 1976
1975 1977
1976 1978 // Journal
1977 1979 .journal.title {
1978 1980 h5 {
1979 1981 float: left;
1980 1982 margin: 0;
1981 1983 width: 70%;
1982 1984 }
1983 1985
1984 1986 ul {
1985 1987 float: right;
1986 1988 display: inline-block;
1987 1989 margin: 0;
1988 1990 width: 30%;
1989 1991 text-align: right;
1990 1992
1991 1993 li {
1992 1994 display: inline;
1993 1995 font-size: @journal-fontsize;
1994 1996 line-height: 1em;
1995 1997
1996 1998 list-style-type: none;
1997 1999 }
1998 2000 }
1999 2001 }
2000 2002
2001 2003 .filterexample {
2002 2004 position: absolute;
2003 2005 top: 95px;
2004 2006 left: @contentpadding;
2005 2007 color: @rcblue;
2006 2008 font-size: 11px;
2007 2009 font-family: @text-regular;
2008 2010 cursor: help;
2009 2011
2010 2012 &:hover {
2011 2013 color: @rcdarkblue;
2012 2014 }
2013 2015
2014 2016 @media (max-width:768px) {
2015 2017 position: relative;
2016 2018 top: auto;
2017 2019 left: auto;
2018 2020 display: block;
2019 2021 }
2020 2022 }
2021 2023
2022 2024
2023 2025 #journal{
2024 2026 margin-bottom: @space;
2025 2027
2026 2028 .journal_day{
2027 2029 margin-bottom: @textmargin/2;
2028 2030 padding-bottom: @textmargin/2;
2029 2031 font-size: @journal-fontsize;
2030 2032 border-bottom: @border-thickness solid @border-default-color;
2031 2033 }
2032 2034
2033 2035 .journal_container{
2034 2036 margin-bottom: @space;
2035 2037
2036 2038 .journal_user{
2037 2039 display: inline-block;
2038 2040 }
2039 2041 .journal_action_container{
2040 2042 display: block;
2041 2043 margin-top: @textmargin;
2042 2044
2043 2045 div{
2044 2046 display: inline;
2045 2047 }
2046 2048
2047 2049 div.journal_action_params{
2048 2050 display: block;
2049 2051 }
2050 2052
2051 2053 div.journal_repo:after{
2052 2054 content: "\A";
2053 2055 white-space: pre;
2054 2056 }
2055 2057
2056 2058 div.date{
2057 2059 display: block;
2058 2060 margin-bottom: @textmargin;
2059 2061 }
2060 2062 }
2061 2063 }
2062 2064 }
2063 2065
2064 2066 // Files
2065 2067 .edit-file-title {
2066 border-bottom: @border-thickness solid @border-default-color;
2067
2068 .breadcrumbs {
2069 margin-bottom: 0;
2068 font-size: 16px;
2069
2070 .title-heading {
2071 padding: 2px;
2070 2072 }
2071 2073 }
2072 2074
2073 2075 .edit-file-fieldset {
2074 margin-top: @sidebarpadding;
2076 margin: @sidebarpadding 0;
2075 2077
2076 2078 .fieldset {
2077 2079 .left-label {
2078 2080 width: 13%;
2079 2081 }
2080 2082 .right-content {
2081 2083 width: 87%;
2082 2084 max-width: 100%;
2083 2085 }
2084 2086 .filename-label {
2085 2087 margin-top: 13px;
2086 2088 }
2087 2089 .commit-message-label {
2088 2090 margin-top: 4px;
2089 2091 }
2090 2092 .file-upload-input {
2091 2093 input {
2092 2094 display: none;
2093 2095 }
2094 2096 margin-top: 10px;
2095 2097 }
2096 2098 .file-upload-label {
2097 2099 margin-top: 10px;
2098 2100 }
2099 2101 p {
2100 2102 margin-top: 5px;
2101 2103 }
2102 2104
2103 2105 }
2104 2106 .custom-path-link {
2105 2107 margin-left: 5px;
2106 2108 }
2107 2109 #commit {
2108 2110 resize: vertical;
2109 2111 }
2110 2112 }
2111 2113
2112 2114 .delete-file-preview {
2113 2115 max-height: 250px;
2114 2116 }
2115 2117
2116 2118 .new-file,
2117 2119 #filter_activate,
2118 2120 #filter_deactivate {
2119 2121 float: right;
2120 2122 margin: 0 0 0 10px;
2121 2123 }
2122 2124
2125 .file-upload-transaction-wrapper {
2126 margin-top: 57px;
2127 clear: both;
2128 }
2129
2130 .file-upload-transaction-wrapper .error {
2131 color: @color5;
2132 }
2133
2134 .file-upload-transaction {
2135 min-height: 200px;
2136 padding: 54px;
2137 border: 1px solid @grey5;
2138 text-align: center;
2139 clear: both;
2140 }
2141
2142 .file-upload-transaction i {
2143 font-size: 48px
2144 }
2145
2123 2146 h3.files_location{
2124 2147 line-height: 2.4em;
2125 2148 }
2126 2149
2127 2150 .browser-nav {
2128 2151 width: 100%;
2129 2152 display: table;
2130 2153 margin-bottom: 20px;
2131 2154
2132 2155 .info_box {
2133 2156 float: left;
2134 2157 display: inline-table;
2135 2158 height: 2.5em;
2136 2159
2137 2160 .browser-cur-rev, .info_box_elem {
2138 2161 display: table-cell;
2139 2162 vertical-align: middle;
2140 2163 }
2141 2164
2142 2165 .drop-menu {
2143 2166 margin: 0 10px;
2144 2167 }
2145 2168
2146 2169 .info_box_elem {
2147 2170 border-top: @border-thickness solid @grey5;
2148 2171 border-bottom: @border-thickness solid @grey5;
2149 2172 box-shadow: @button-shadow;
2150 2173
2151 2174 #at_rev, a {
2152 2175 padding: 0.6em 0.4em;
2153 2176 margin: 0;
2154 2177 .box-shadow(none);
2155 2178 border: 0;
2156 2179 height: 12px;
2157 2180 color: @grey2;
2158 2181 }
2159 2182
2160 2183 input#at_rev {
2161 2184 max-width: 50px;
2162 2185 text-align: center;
2163 2186 }
2164 2187
2165 2188 &.previous {
2166 2189 border: @border-thickness solid @grey5;
2167 2190 border-top-left-radius: @border-radius;
2168 2191 border-bottom-left-radius: @border-radius;
2169 2192
2170 2193 &:hover {
2171 2194 border-color: @grey4;
2172 2195 }
2173 2196
2174 2197 .disabled {
2175 2198 color: @grey5;
2176 2199 cursor: not-allowed;
2177 2200 opacity: 0.5;
2178 2201 }
2179 2202 }
2180 2203
2181 2204 &.next {
2182 2205 border: @border-thickness solid @grey5;
2183 2206 border-top-right-radius: @border-radius;
2184 2207 border-bottom-right-radius: @border-radius;
2185 2208
2186 2209 &:hover {
2187 2210 border-color: @grey4;
2188 2211 }
2189 2212
2190 2213 .disabled {
2191 2214 color: @grey5;
2192 2215 cursor: not-allowed;
2193 2216 opacity: 0.5;
2194 2217 }
2195 2218 }
2196 2219 }
2197 2220
2198 2221 .browser-cur-rev {
2199 2222
2200 2223 span{
2201 2224 margin: 0;
2202 2225 color: @rcblue;
2203 2226 height: 12px;
2204 2227 display: inline-block;
2205 2228 padding: 0.7em 1em ;
2206 2229 border: @border-thickness solid @rcblue;
2207 2230 margin-right: @padding;
2208 2231 }
2209 2232 }
2210 2233
2211 2234 }
2212 2235
2213 2236 .select-index-number {
2214 2237 margin: 0 0 0 20px;
2215 2238 color: @grey3;
2216 2239 }
2217 2240
2218 2241 .search_activate {
2219 2242 display: table-cell;
2220 2243 vertical-align: middle;
2221 2244
2222 2245 input, label{
2223 2246 margin: 0;
2224 2247 padding: 0;
2225 2248 }
2226 2249
2227 2250 input{
2228 2251 margin-left: @textmargin;
2229 2252 }
2230 2253
2231 2254 }
2232 2255 }
2233 2256
2234 2257 .browser-cur-rev{
2235 2258 margin-bottom: @textmargin;
2236 2259 }
2237 2260
2238 2261 #node_filter_box_loading{
2239 2262 .info_text;
2240 2263 }
2241 2264
2242 2265 .browser-search {
2243 2266 margin: -25px 0px 5px 0px;
2244 2267 }
2245 2268
2246 2269 .files-quick-filter {
2247 2270 float: right;
2248 2271 width: 180px;
2249 2272 position: relative;
2250 2273 }
2251 2274
2252 2275 .files-filter-box {
2253 2276 display: flex;
2254 2277 padding: 0px;
2255 2278 border-radius: 3px;
2256 2279 margin-bottom: 0;
2257 2280
2258 2281 a {
2259 2282 border: none !important;
2260 2283 }
2261 2284
2262 2285 li {
2263 2286 list-style-type: none
2264 2287 }
2265 2288 }
2266 2289
2267 2290 .files-filter-box-path {
2268 2291 line-height: 33px;
2269 2292 padding: 0;
2270 2293 width: 20px;
2271 2294 position: absolute;
2272 2295 z-index: 11;
2273 2296 left: 5px;
2274 2297 }
2275 2298
2276 2299 .files-filter-box-input {
2277 2300 margin-right: 0;
2278 2301
2279 2302 input {
2280 2303 border: 1px solid @white;
2281 2304 padding-left: 25px;
2282 2305 width: 145px;
2283 2306
2284 2307 &:hover {
2285 2308 border-color: @grey6;
2286 2309 }
2287 2310
2288 2311 &:focus {
2289 2312 border-color: @grey5;
2290 2313 }
2291 2314 }
2292 2315 }
2293 2316
2294 2317 .browser-result{
2295 2318 td a{
2296 2319 margin-left: 0.5em;
2297 2320 display: inline-block;
2298 2321
2299 2322 em {
2300 2323 font-weight: @text-bold-weight;
2301 2324 font-family: @text-bold;
2302 2325 }
2303 2326 }
2304 2327 }
2305 2328
2306 2329 .browser-highlight{
2307 2330 background-color: @grey5-alpha;
2308 2331 }
2309 2332
2310 2333
2334 .edit-file-fieldset #location,
2335 .edit-file-fieldset #filename {
2336 display: flex;
2337 width: -moz-available; /* WebKit-based browsers will ignore this. */
2338 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2339 width: fill-available;
2340 border: 0;
2341 }
2342
2343 .path-items {
2344 display: flex;
2345 padding: 0;
2346 border: 1px solid #eeeeee;
2347 width: 100%;
2348 float: left;
2349
2350 .breadcrumb-path {
2351 line-height: 30px;
2352 padding: 0 4px;
2353 white-space: nowrap;
2354 }
2355
2356 .location-path {
2357 width: -moz-available; /* WebKit-based browsers will ignore this. */
2358 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2359 width: fill-available;
2360
2361 .file-name-input {
2362 padding: 0.5em 0;
2363 }
2364
2365 }
2366
2367 ul {
2368 display: flex;
2369 margin: 0;
2370 padding: 0;
2371 }
2372 li {
2373 list-style-type: none;
2374 }
2375 }
2376
2377 .editor-items {
2378 height: 40px;
2379 margin: 10px 0 -17px 10px;
2380
2381 .editor-action {
2382 cursor: pointer;
2383 }
2384
2385 .editor-action.active {
2386 border-bottom: 2px solid #5C5C5C;
2387 }
2388
2389 li {
2390 list-style-type: none;
2391 }
2392 }
2393
2394 .edit-file-fieldset .message textarea {
2395 border: 1px solid #eeeeee;
2396 }
2397
2398 #files_data .codeblock {
2399 background-color: #F5F5F5;
2400 }
2401
2402 #editor_preview {
2403 background: white;
2404 }
2405
2406 .show-editor {
2407 padding: 10px;
2408 background-color: white;
2409
2410 }
2411
2412 .show-preview {
2413 padding: 10px;
2414 background-color: white;
2415 border-left: 1px solid #eeeeee;
2416 }
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2311 2429 // Search
2312 2430
2313 2431 .search-form{
2314 2432 #q {
2315 2433 width: @search-form-width;
2316 2434 }
2317 2435 .fields{
2318 2436 margin: 0 0 @space;
2319 2437 }
2320 2438
2321 2439 label{
2322 2440 display: inline-block;
2323 2441 margin-right: @textmargin;
2324 2442 padding-top: 0.25em;
2325 2443 }
2326 2444
2327 2445
2328 2446 .results{
2329 2447 clear: both;
2330 2448 margin: 0 0 @padding;
2331 2449 }
2332 2450
2333 2451 .search-tags {
2334 2452 padding: 5px 0;
2335 2453 }
2336 2454 }
2337 2455
2338 2456 div.search-feedback-items {
2339 2457 display: inline-block;
2340 2458 }
2341 2459
2342 2460 div.search-code-body {
2343 2461 background-color: #ffffff; padding: 5px 0 5px 10px;
2344 2462 pre {
2345 2463 .match { background-color: #faffa6;}
2346 2464 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2347 2465 }
2348 2466 }
2349 2467
2350 2468 .expand_commit.search {
2351 2469 .show_more.open {
2352 2470 height: auto;
2353 2471 max-height: none;
2354 2472 }
2355 2473 }
2356 2474
2357 2475 .search-results {
2358 2476
2359 2477 h2 {
2360 2478 margin-bottom: 0;
2361 2479 }
2362 2480 .codeblock {
2363 2481 border: none;
2364 2482 background: transparent;
2365 2483 }
2366 2484
2367 2485 .codeblock-header {
2368 2486 border: none;
2369 2487 background: transparent;
2370 2488 }
2371 2489
2372 2490 .code-body {
2373 2491 border: @border-thickness solid @grey6;
2374 2492 .border-radius(@border-radius);
2375 2493 }
2376 2494
2377 2495 .td-commit {
2378 2496 &:extend(pre);
2379 2497 border-bottom: @border-thickness solid @border-default-color;
2380 2498 }
2381 2499
2382 2500 .message {
2383 2501 height: auto;
2384 2502 max-width: 350px;
2385 2503 white-space: normal;
2386 2504 text-overflow: initial;
2387 2505 overflow: visible;
2388 2506
2389 2507 .match { background-color: #faffa6;}
2390 2508 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2391 2509 }
2392 2510
2393 2511 .path {
2394 2512 border-bottom: none !important;
2395 2513 border-left: 1px solid @grey6 !important;
2396 2514 border-right: 1px solid @grey6 !important;
2397 2515 }
2398 2516 }
2399 2517
2400 2518 table.rctable td.td-search-results div {
2401 2519 max-width: 100%;
2402 2520 }
2403 2521
2404 2522 #tip-box, .tip-box{
2405 2523 padding: @menupadding/2;
2406 2524 display: block;
2407 2525 border: @border-thickness solid @border-highlight-color;
2408 2526 .border-radius(@border-radius);
2409 2527 background-color: white;
2410 2528 z-index: 99;
2411 2529 white-space: pre-wrap;
2412 2530 }
2413 2531
2414 2532 #linktt {
2415 2533 width: 79px;
2416 2534 }
2417 2535
2418 2536 #help_kb .modal-content{
2419 2537 max-width: 750px;
2420 2538 margin: 10% auto;
2421 2539
2422 2540 table{
2423 2541 td,th{
2424 2542 border-bottom: none;
2425 2543 line-height: 2.5em;
2426 2544 }
2427 2545 th{
2428 2546 padding-bottom: @textmargin/2;
2429 2547 }
2430 2548 td.keys{
2431 2549 text-align: center;
2432 2550 }
2433 2551 }
2434 2552
2435 2553 .block-left{
2436 2554 width: 45%;
2437 2555 margin-right: 5%;
2438 2556 }
2439 2557 .modal-footer{
2440 2558 clear: both;
2441 2559 }
2442 2560 .key.tag{
2443 2561 padding: 0.5em;
2444 2562 background-color: @rcblue;
2445 2563 color: white;
2446 2564 border-color: @rcblue;
2447 2565 .box-shadow(none);
2448 2566 }
2449 2567 }
2450 2568
2451 2569
2452 2570
2453 2571 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2454 2572
2455 2573 @import 'statistics-graph';
2456 2574 @import 'tables';
2457 2575 @import 'forms';
2458 2576 @import 'diff';
2459 2577 @import 'summary';
2460 2578 @import 'navigation';
2461 2579
2462 2580 //--- SHOW/HIDE SECTIONS --//
2463 2581
2464 2582 .btn-collapse {
2465 2583 float: right;
2466 2584 text-align: right;
2467 2585 font-family: @text-light;
2468 2586 font-size: @basefontsize;
2469 2587 cursor: pointer;
2470 2588 border: none;
2471 2589 color: @rcblue;
2472 2590 }
2473 2591
2474 2592 table.rctable,
2475 2593 table.dataTable {
2476 2594 .btn-collapse {
2477 2595 float: right;
2478 2596 text-align: right;
2479 2597 }
2480 2598 }
2481 2599
2482 2600 table.rctable {
2483 2601 &.permissions {
2484 2602
2485 2603 th.td-owner {
2486 2604 padding: 0;
2487 2605 }
2488 2606
2489 2607 th {
2490 2608 font-weight: normal;
2491 2609 padding: 0 5px;
2492 2610 }
2493 2611
2494 2612 }
2495 2613 }
2496 2614
2497 2615
2498 2616 // TODO: johbo: Fix for IE10, this avoids that we see a border
2499 2617 // and padding around checkboxes and radio boxes. Move to the right place,
2500 2618 // or better: Remove this once we did the form refactoring.
2501 2619 input[type=checkbox],
2502 2620 input[type=radio] {
2503 2621 padding: 0;
2504 2622 border: none;
2505 2623 }
2506 2624
2507 2625 .toggle-ajax-spinner{
2508 2626 height: 16px;
2509 2627 width: 16px;
2510 2628 }
2511 2629
2512 2630
2513 2631 .markup-form .clearfix {
2514 2632 .border-radius(@border-radius);
2515 2633 margin: 0px;
2516 2634 }
2517 2635
2518 2636 .markup-form-area {
2519 2637 padding: 8px 12px;
2520 2638 border: 1px solid @grey4;
2521 2639 .border-radius(@border-radius);
2522 2640 }
2523 2641
2524 2642 .markup-form-area-header .nav-links {
2525 2643 display: flex;
2526 2644 flex-flow: row wrap;
2527 2645 -webkit-flex-flow: row wrap;
2528 2646 width: 100%;
2529 2647 }
2530 2648
2531 2649 .markup-form-area-footer {
2532 2650 display: flex;
2533 2651 }
2534 2652
2535 2653 .markup-form-area-footer .toolbar {
2536 2654
2537 2655 }
2538 2656
2539 2657 // markup Form
2540 2658 div.markup-form {
2541 2659 margin-top: 20px;
2542 2660 }
2543 2661
2544 2662 .markup-form strong {
2545 2663 display: block;
2546 2664 margin-bottom: 15px;
2547 2665 }
2548 2666
2549 2667 .markup-form textarea {
2550 2668 width: 100%;
2551 2669 height: 100px;
2552 2670 font-family: @text-monospace;
2553 2671 }
2554 2672
2555 2673 form.markup-form {
2556 2674 margin-top: 10px;
2557 2675 margin-left: 10px;
2558 2676 }
2559 2677
2560 2678 .markup-form .comment-block-ta,
2561 2679 .markup-form .preview-box {
2562 2680 .border-radius(@border-radius);
2563 2681 .box-sizing(border-box);
2564 2682 background-color: white;
2565 2683 }
2566 2684
2567 2685 .markup-form .preview-box.unloaded {
2568 2686 height: 50px;
2569 2687 text-align: center;
2570 2688 padding: 20px;
2571 2689 background-color: white;
2572 2690 }
2573 2691
2574 2692
2575 2693 .dropzone-wrapper {
2576 2694 border: 1px solid @grey5;
2577 2695 padding: 20px;
2578 2696 }
2579 2697
2580 .dropzone {
2698 .dropzone,
2699 .dropzone-pure {
2581 2700 border: 2px dashed @grey5;
2582 2701 border-radius: 5px;
2583 2702 background: white;
2584 2703 min-height: 200px;
2585 2704 padding: 54px;
2586 }
2587 .dropzone .dz-message {
2588 font-weight: 700;
2589 }
2590
2591 .dropzone .dz-message {
2592 text-align: center;
2593 margin: 2em 0;
2705
2706 .dz-message {
2707 font-weight: 700;
2708 text-align: center;
2709 margin: 2em 0;
2710 }
2711
2594 2712 }
2595 2713
2596 2714 .dz-preview {
2597 margin: 10px 0px !important;
2715 margin: 10px 0 !important;
2598 2716 position: relative;
2599 2717 vertical-align: top;
2600 2718 padding: 10px;
2719 border-bottom: 1px solid @grey5;
2601 2720 }
2602 2721
2603 2722 .dz-filename {
2604 2723 font-weight: 700;
2605 2724 float:left;
2606 2725 }
2607 2726
2727 .dz-sending {
2728 float: right;
2729 }
2730
2608 2731 .dz-response {
2609 2732 clear:both
2610 2733 }
2611 2734
2612 2735 .dz-filename-size {
2613 2736 float:right
2614 2737 }
2615 2738
2616 2739 .dz-error-message {
2617 2740 color: @alert2;
2618 } No newline at end of file
2741 padding-top: 10px;
2742 clear: both;
2743 }
@@ -1,374 +1,375 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
18 18 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
19 19 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
20 20 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
21 21 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
22 22 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
23 23 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
25 25 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
27 27 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
28 28 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
29 29 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
30 30 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
32 32 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
33 33 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
34 34 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
35 35 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
36 36 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
37 37 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
38 38 pyroutes.register('admin_home', '/_admin', []);
39 39 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
40 40 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
41 41 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
42 42 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
43 43 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
44 44 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
45 45 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
46 46 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
47 47 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
48 48 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
49 49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
50 50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 51 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
52 52 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
53 53 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
54 54 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
55 55 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
56 56 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
57 57 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
58 58 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
59 59 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
60 60 pyroutes.register('admin_settings', '/_admin/settings', []);
61 61 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
62 62 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
63 63 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
64 64 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
65 65 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
66 66 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
67 67 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
68 68 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
69 69 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
70 70 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
71 71 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
72 72 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
73 73 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
74 74 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
75 75 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
76 76 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
77 77 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
78 78 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
79 79 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
80 80 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
81 81 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
82 82 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
83 83 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
84 84 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
85 85 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
86 86 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
87 87 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
88 88 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
89 89 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
90 90 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
91 91 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
92 92 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
93 93 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
94 94 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
95 95 pyroutes.register('users', '/_admin/users', []);
96 96 pyroutes.register('users_data', '/_admin/users_data', []);
97 97 pyroutes.register('users_create', '/_admin/users/create', []);
98 98 pyroutes.register('users_new', '/_admin/users/new', []);
99 99 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
100 100 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
101 101 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
102 102 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
103 103 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
104 104 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
105 105 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
106 106 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
107 107 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
108 108 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
109 109 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
110 110 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
111 111 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
112 112 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
113 113 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
114 114 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
115 115 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
116 116 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
117 117 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
118 118 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
119 119 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
120 120 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
121 121 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
122 122 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
123 123 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
124 124 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
125 125 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
126 126 pyroutes.register('user_groups', '/_admin/user_groups', []);
127 127 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
128 128 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
129 129 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
130 130 pyroutes.register('repos', '/_admin/repos', []);
131 131 pyroutes.register('repo_new', '/_admin/repos/new', []);
132 132 pyroutes.register('repo_create', '/_admin/repos/create', []);
133 133 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
134 134 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
135 135 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
136 136 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
137 137 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
138 138 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
139 139 pyroutes.register('channelstream_proxy', '/_channelstream', []);
140 140 pyroutes.register('upload_file', '/_file_store/upload', []);
141 141 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
142 142 pyroutes.register('logout', '/_admin/logout', []);
143 143 pyroutes.register('reset_password', '/_admin/password_reset', []);
144 144 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
145 145 pyroutes.register('home', '/', []);
146 146 pyroutes.register('user_autocomplete_data', '/_users', []);
147 147 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
148 148 pyroutes.register('repo_list_data', '/_repos', []);
149 149 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
150 150 pyroutes.register('goto_switcher_data', '/_goto_data', []);
151 151 pyroutes.register('markup_preview', '/_markup_preview', []);
152 pyroutes.register('file_preview', '/_file_preview', []);
152 153 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
153 154 pyroutes.register('journal', '/_admin/journal', []);
154 155 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
155 156 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
156 157 pyroutes.register('journal_public', '/_admin/public_journal', []);
157 158 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
158 159 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
159 160 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
160 161 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
161 162 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
162 163 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
163 164 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
164 165 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
165 166 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
166 167 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
167 168 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
168 169 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
169 170 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
170 171 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
171 172 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
172 173 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
173 174 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
174 175 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
175 176 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
176 177 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
177 178 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
178 179 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
179 180 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
180 181 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
181 182 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
182 183 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
183 184 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
184 185 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
185 186 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
186 187 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
187 188 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
188 189 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
189 190 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
190 191 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 192 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 193 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 194 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 195 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
195 196 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 197 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 198 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 199 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 200 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 201 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 202 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
202 203 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
203 204 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
204 205 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
205 206 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 207 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
207 208 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 209 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
209 210 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 211 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
211 212 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
212 213 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
213 214 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
214 215 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
215 216 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
216 217 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
217 218 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
218 219 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
219 220 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
220 221 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
221 222 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
222 223 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
223 224 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
224 225 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
225 226 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
226 227 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
227 228 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
228 229 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
229 230 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
230 231 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
231 232 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
232 233 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
233 234 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
234 235 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
235 236 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
236 237 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
237 238 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
238 239 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
239 240 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
240 241 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
241 242 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
242 243 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
243 244 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
244 245 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
245 246 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
246 247 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
247 248 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
248 249 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
249 250 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
250 251 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
251 252 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
252 253 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
253 254 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
254 255 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
255 256 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
256 257 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
257 258 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
258 259 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
259 260 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
260 261 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
261 262 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
262 263 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
263 264 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
264 265 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
265 266 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
266 267 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
267 268 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
268 269 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
269 270 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
270 271 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
271 272 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
272 273 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
273 274 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
274 275 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
275 276 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
276 277 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
277 278 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
278 279 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
279 280 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
280 281 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
281 282 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
282 283 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
283 284 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
284 285 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
285 286 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
286 287 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
287 288 pyroutes.register('search', '/_admin/search', []);
288 289 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
289 290 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
290 291 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
291 292 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
292 293 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
293 294 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
294 295 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
295 296 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
296 297 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
297 298 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
298 299 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
299 300 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
300 301 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
301 302 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
302 303 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
303 304 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
304 305 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
305 306 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
306 307 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
307 308 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
308 309 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
309 310 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
310 311 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
311 312 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
312 313 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
313 314 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
314 315 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
315 316 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
316 317 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
317 318 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
318 319 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
319 320 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
320 321 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
321 322 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
322 323 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
323 324 pyroutes.register('gists_show', '/_admin/gists', []);
324 325 pyroutes.register('gists_new', '/_admin/gists/new', []);
325 326 pyroutes.register('gists_create', '/_admin/gists/create', []);
326 327 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
327 328 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
328 329 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
329 330 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
330 331 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
331 332 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
332 333 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
333 334 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
334 335 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
335 336 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
336 337 pyroutes.register('apiv2', '/_admin/api', []);
337 338 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
338 339 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
339 340 pyroutes.register('login', '/_admin/login', []);
340 341 pyroutes.register('register', '/_admin/register', []);
341 342 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
342 343 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
343 344 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
344 345 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
345 346 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
346 347 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
347 348 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
348 349 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
349 350 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
350 351 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
351 352 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
352 353 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
353 354 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
354 355 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
355 356 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
356 357 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
357 358 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
358 359 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
359 360 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
360 361 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
361 362 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
362 363 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
363 364 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
364 365 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
365 366 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
366 367 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
367 368 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
368 369 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
369 370 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
370 371 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
371 372 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
372 373 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
373 374 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
374 375 }
@@ -1,850 +1,843 b''
1 1 // # Copyright (C) 2010-2019 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 * Code Mirror
21 21 */
22 22 // global code-mirror logger;, to enable run
23 23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
24 24
25 25 cmLog = Logger.get('CodeMirror');
26 26 cmLog.setLevel(Logger.OFF);
27 27
28 28
29 29 //global cache for inline forms
30 30 var userHintsCache = {};
31 31
32 32 // global timer, used to cancel async loading
33 33 var CodeMirrorLoadUserHintTimer;
34 34
35 35 var escapeRegExChars = function(value) {
36 36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
37 37 };
38 38
39 39 /**
40 40 * Load hints from external source returns an array of objects in a format
41 41 * that hinting lib requires
42 42 * @returns {Array}
43 43 */
44 44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
45 45 cmLog.debug('Loading mentions users via AJAX');
46 46 var _users = [];
47 47 $.ajax({
48 48 type: 'GET',
49 49 data: {query: query},
50 50 url: pyroutes.url('user_autocomplete_data'),
51 51 headers: {'X-PARTIAL-XHR': true},
52 52 async: true
53 53 })
54 54 .done(function(data) {
55 55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
56 56 $.each(data.suggestions, function(i) {
57 57 var userObj = data.suggestions[i];
58 58
59 59 if (userObj.username !== "default") {
60 60 _users.push({
61 61 text: userObj.username + " ",
62 62 org_text: userObj.username,
63 63 displayText: userObj.value_display, // search that field
64 64 // internal caches
65 65 _icon_link: userObj.icon_link,
66 66 _text: userObj.value_display,
67 67
68 68 render: function(elt, data, completion) {
69 69 var el = document.createElement('div');
70 70 el.className = "CodeMirror-hint-entry";
71 71 el.innerHTML = tmpl.format(
72 72 completion._icon_link, completion._text);
73 73 elt.appendChild(el);
74 74 }
75 75 });
76 76 }
77 77 });
78 78 cmLog.debug('Mention users loaded');
79 79 // set to global cache
80 80 userHintsCache[query] = _users;
81 81 triggerHints(userHintsCache[query]);
82 82 })
83 83 .fail(function(data, textStatus, xhr) {
84 84 alert("error processing request. \n" +
85 85 "Error code {0} ({1}).".format(data.status, data.statusText));
86 86 });
87 87 };
88 88
89 89 /**
90 90 * filters the results based on the current context
91 91 * @param users
92 92 * @param context
93 93 * @returns {Array}
94 94 */
95 95 var CodeMirrorFilterUsers = function(users, context) {
96 96 var MAX_LIMIT = 10;
97 97 var filtered_users = [];
98 98 var curWord = context.string;
99 99
100 100 cmLog.debug('Filtering users based on query:', curWord);
101 101 $.each(users, function(i) {
102 102 var match = users[i];
103 103 var searchText = match.displayText;
104 104
105 105 if (!curWord ||
106 106 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
107 107 // reset state
108 108 match._text = match.displayText;
109 109 if (curWord) {
110 110 // do highlighting
111 111 var pattern = '(' + escapeRegExChars(curWord) + ')';
112 112 match._text = searchText.replace(
113 113 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
114 114 }
115 115
116 116 filtered_users.push(match);
117 117 }
118 118 // to not return to many results, use limit of filtered results
119 119 if (filtered_users.length > MAX_LIMIT) {
120 120 return false;
121 121 }
122 122 });
123 123
124 124 return filtered_users;
125 125 };
126 126
127 127 var CodeMirrorMentionHint = function(editor, callback, options) {
128 128 var cur = editor.getCursor();
129 129 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
130 130
131 131 // match on @ +1char
132 132 var tokenMatch = new RegExp(
133 133 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
134 134
135 135 var tokenStr = '';
136 136 if (tokenMatch !== null && tokenMatch.length > 0){
137 137 tokenStr = tokenMatch[0].strip();
138 138 } else {
139 139 // skip if we didn't match our token
140 140 return;
141 141 }
142 142
143 143 var context = {
144 144 start: (cur.ch - tokenStr.length) + 1,
145 145 end: cur.ch,
146 146 string: tokenStr.slice(1),
147 147 type: null
148 148 };
149 149
150 150 // case when we put the @sign in fron of a string,
151 151 // eg <@ we put it here>sometext then we need to prepend to text
152 152 if (context.end > cur.ch) {
153 153 context.start = context.start + 1; // we add to the @ sign
154 154 context.end = cur.ch; // don't eat front part just append
155 155 context.string = context.string.slice(1, cur.ch - context.start);
156 156 }
157 157
158 158 cmLog.debug('Mention context', context);
159 159
160 160 var triggerHints = function(userHints){
161 161 return callback({
162 162 list: CodeMirrorFilterUsers(userHints, context),
163 163 from: CodeMirror.Pos(cur.line, context.start),
164 164 to: CodeMirror.Pos(cur.line, context.end)
165 165 });
166 166 };
167 167
168 168 var queryBasedHintsCache = undefined;
169 169 // if we have something in the cache, try to fetch the query based cache
170 170 if (userHintsCache !== {}){
171 171 queryBasedHintsCache = userHintsCache[context.string];
172 172 }
173 173
174 174 if (queryBasedHintsCache !== undefined) {
175 175 cmLog.debug('Users loaded from cache');
176 176 triggerHints(queryBasedHintsCache);
177 177 } else {
178 178 // this takes care for async loading, and then displaying results
179 179 // and also propagates the userHintsCache
180 180 window.clearTimeout(CodeMirrorLoadUserHintTimer);
181 181 CodeMirrorLoadUserHintTimer = setTimeout(function() {
182 182 CodeMirrorLoadUserHints(context.string, triggerHints);
183 183 }, 300);
184 184 }
185 185 };
186 186
187 187 var CodeMirrorCompleteAfter = function(cm, pred) {
188 188 var options = {
189 189 completeSingle: false,
190 190 async: true,
191 191 closeOnUnfocus: true
192 192 };
193 193 var cur = cm.getCursor();
194 194 setTimeout(function() {
195 195 if (!cm.state.completionActive) {
196 196 cmLog.debug('Trigger mentions hinting');
197 197 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
198 198 }
199 199 }, 100);
200 200
201 201 // tell CodeMirror we didn't handle the key
202 202 // trick to trigger on a char but still complete it
203 203 return CodeMirror.Pass;
204 204 };
205 205
206 206 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
207 var ta = $('#' + textAreadId).get(0);
207 if (textAreadId.substr(0,1) === "#"){
208 var ta = $(textAreadId).get(0);
209 }else {
210 var ta = $('#' + textAreadId).get(0);
211 }
212
208 213 if (focus === undefined) {
209 214 focus = true;
210 215 }
211 216
212 217 // default options
213 218 var codeMirrorOptions = {
214 219 mode: "null",
215 220 lineNumbers: true,
216 221 indentUnit: 4,
217 222 autofocus: focus
218 223 };
219 224
220 225 if (options !== undefined) {
221 226 // extend with custom options
222 227 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
223 228 }
224 229
225 230 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
226 231
227 232 $('#reset').on('click', function(e) {
228 233 window.location = resetUrl;
229 234 });
230 235
231 236 return myCodeMirror;
232 237 };
233 238
234 239
235 240 var initMarkupCodeMirror = function(textAreadId, focus, options) {
236 241 var initialHeight = 100;
237 242
238 243 var ta = $(textAreadId).get(0);
239 244 if (focus === undefined) {
240 245 focus = true;
241 246 }
242 247
243 248 // default options
244 249 var codeMirrorOptions = {
245 250 lineNumbers: false,
246 251 indentUnit: 4,
247 252 viewportMargin: 30,
248 253 // this is a trick to trigger some logic behind codemirror placeholder
249 254 // it influences styling and behaviour.
250 255 placeholder: " ",
251 256 lineWrapping: true,
252 257 autofocus: focus
253 258 };
254 259
255 260 if (options !== undefined) {
256 261 // extend with custom options
257 262 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
258 263 }
259 264
260 265 var cm = CodeMirror.fromTextArea(ta, codeMirrorOptions);
261 266 cm.setSize(null, initialHeight);
262 267 cm.setOption("mode", DEFAULT_RENDERER);
263 268 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
264 269 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
265 270
266 271 // start listening on changes to make auto-expanded editor
267 272 cm.on("change", function(instance, changeObj) {
268 273 var height = initialHeight;
269 274 var lines = instance.lineCount();
270 275 if ( lines > 6 && lines < 20) {
271 276 height = "auto";
272 277 }
273 278 else if (lines >= 20){
274 279 zheight = 20*15;
275 280 }
276 281 instance.setSize(null, height);
277 282
278 283 // detect if the change was trigger by auto desc, or user input
279 284 var changeOrigin = changeObj.origin;
280 285
281 286 if (changeOrigin === "setValue") {
282 287 cmLog.debug('Change triggered by setValue');
283 288 }
284 289 else {
285 290 cmLog.debug('user triggered change !');
286 291 // set special marker to indicate user has created an input.
287 292 instance._userDefinedValue = true;
288 293 }
289 294
290 295 });
291 296
292 297 return cm;
293 298 };
294 299
295 300
296 301 var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){
297 302 var initialHeight = 100;
298 303
299 304 if (typeof userHintsCache === "undefined") {
300 305 userHintsCache = {};
301 306 cmLog.debug('Init empty cache for mentions');
302 307 }
303 308 if (!$(textAreaId).get(0)) {
304 309 cmLog.debug('Element for textarea not found', textAreaId);
305 310 return;
306 311 }
307 312 /**
308 313 * Filter action based on typed in text
309 314 * @param actions
310 315 * @param context
311 316 * @returns {Array}
312 317 */
313 318
314 319 var filterActions = function(actions, context){
315 320
316 321 var MAX_LIMIT = 10;
317 322 var filtered_actions = [];
318 323 var curWord = context.string;
319 324
320 325 cmLog.debug('Filtering actions based on query:', curWord);
321 326 $.each(actions, function(i) {
322 327 var match = actions[i];
323 328 var searchText = match.searchText;
324 329
325 330 if (!curWord ||
326 331 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
327 332 // reset state
328 333 match._text = match.displayText;
329 334 if (curWord) {
330 335 // do highlighting
331 336 var pattern = '(' + escapeRegExChars(curWord) + ')';
332 337 match._text = searchText.replace(
333 338 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
334 339 }
335 340
336 341 filtered_actions.push(match);
337 342 }
338 343 // to not return to many results, use limit of filtered results
339 344 if (filtered_actions.length > MAX_LIMIT) {
340 345 return false;
341 346 }
342 347 });
343 348
344 349 return filtered_actions;
345 350 };
346 351
347 352 var submitForm = function(cm, pred) {
348 353 $(cm.display.input.textarea.form).submit();
349 354 return CodeMirror.Pass;
350 355 };
351 356
352 357 var completeActions = function(actions){
353 358
354 359 var registeredActions = [];
355 360 var allActions = [
356 361 {
357 362 text: "approve",
358 363 searchText: "status approved",
359 364 displayText: _gettext('Set status to Approved'),
360 365 hint: function(CodeMirror, data, completion) {
361 366 CodeMirror.replaceRange("", completion.from || data.from,
362 367 completion.to || data.to, "complete");
363 368 $(CommentForm.statusChange).select2("val", 'approved').trigger('change');
364 369 },
365 370 render: function(elt, data, completion) {
366 371 var el = document.createElement('div');
367 372 el.className = "flag_status flag_status_comment_box approved pull-left";
368 373 elt.appendChild(el);
369 374
370 375 el = document.createElement('span');
371 376 el.innerHTML = completion.displayText;
372 377 elt.appendChild(el);
373 378 }
374 379 },
375 380 {
376 381 text: "reject",
377 382 searchText: "status rejected",
378 383 displayText: _gettext('Set status to Rejected'),
379 384 hint: function(CodeMirror, data, completion) {
380 385 CodeMirror.replaceRange("", completion.from || data.from,
381 386 completion.to || data.to, "complete");
382 387 $(CommentForm.statusChange).select2("val", 'rejected').trigger('change');
383 388 },
384 389 render: function(elt, data, completion) {
385 390 var el = document.createElement('div');
386 391 el.className = "flag_status flag_status_comment_box rejected pull-left";
387 392 elt.appendChild(el);
388 393
389 394 el = document.createElement('span');
390 395 el.innerHTML = completion.displayText;
391 396 elt.appendChild(el);
392 397 }
393 398 },
394 399 {
395 400 text: "as_todo",
396 401 searchText: "todo comment",
397 402 displayText: _gettext('TODO comment'),
398 403 hint: function(CodeMirror, data, completion) {
399 404 CodeMirror.replaceRange("", completion.from || data.from,
400 405 completion.to || data.to, "complete");
401 406
402 407 $(CommentForm.commentType).val('todo');
403 408 },
404 409 render: function(elt, data, completion) {
405 410 var el = document.createElement('div');
406 411 el.className = "pull-left";
407 412 elt.appendChild(el);
408 413
409 414 el = document.createElement('span');
410 415 el.innerHTML = completion.displayText;
411 416 elt.appendChild(el);
412 417 }
413 418 },
414 419 {
415 420 text: "as_note",
416 421 searchText: "note comment",
417 422 displayText: _gettext('Note Comment'),
418 423 hint: function(CodeMirror, data, completion) {
419 424 CodeMirror.replaceRange("", completion.from || data.from,
420 425 completion.to || data.to, "complete");
421 426
422 427 $(CommentForm.commentType).val('note');
423 428 },
424 429 render: function(elt, data, completion) {
425 430 var el = document.createElement('div');
426 431 el.className = "pull-left";
427 432 elt.appendChild(el);
428 433
429 434 el = document.createElement('span');
430 435 el.innerHTML = completion.displayText;
431 436 elt.appendChild(el);
432 437 }
433 438 }
434 439 ];
435 440
436 441 $.each(allActions, function(index, value){
437 442 var actionData = allActions[index];
438 443 if (actions.indexOf(actionData['text']) != -1) {
439 444 registeredActions.push(actionData);
440 445 }
441 446 });
442 447
443 448 return function(cm, pred) {
444 449 var cur = cm.getCursor();
445 450 var options = {
446 451 closeOnUnfocus: true,
447 452 registeredActions: registeredActions
448 453 };
449 454 setTimeout(function() {
450 455 if (!cm.state.completionActive) {
451 456 cmLog.debug('Trigger actions hinting');
452 457 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
453 458 }
454 459 }, 100);
455 460
456 461 // tell CodeMirror we didn't handle the key
457 462 // trick to trigger on a char but still complete it
458 463 return CodeMirror.Pass;
459 464 }
460 465 };
461 466
462 467 var extraKeys = {
463 468 "'@'": CodeMirrorCompleteAfter,
464 469 Tab: function(cm) {
465 470 // space indent instead of TABS
466 471 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
467 472 cm.replaceSelection(spaces);
468 473 }
469 474 };
470 475 // submit form on Meta-Enter
471 476 if (OSType === "mac") {
472 477 extraKeys["Cmd-Enter"] = submitForm;
473 478 }
474 479 else {
475 480 extraKeys["Ctrl-Enter"] = submitForm;
476 481 }
477 482
478 483 if (triggerActions) {
479 484 // register triggerActions for this instance
480 485 extraKeys["'/'"] = completeActions(triggerActions);
481 486 }
482 487
483 488 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
484 489 lineNumbers: false,
485 490 indentUnit: 4,
486 491 viewportMargin: 30,
487 492 // this is a trick to trigger some logic behind codemirror placeholder
488 493 // it influences styling and behaviour.
489 494 placeholder: " ",
490 495 extraKeys: extraKeys,
491 496 lineWrapping: true
492 497 });
493 498
494 499 cm.setSize(null, initialHeight);
495 500 cm.setOption("mode", DEFAULT_RENDERER);
496 501 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
497 502 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
498 503 // start listening on changes to make auto-expanded editor
499 504 cm.on("change", function(self) {
500 505 var height = initialHeight;
501 506 var lines = self.lineCount();
502 507 if ( lines > 6 && lines < 20) {
503 508 height = "auto";
504 509 }
505 510 else if (lines >= 20){
506 511 zheight = 20*15;
507 512 }
508 513 self.setSize(null, height);
509 514 });
510 515
511 516 var actionHint = function(editor, options) {
512 517
513 518 var cur = editor.getCursor();
514 519 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
515 520
516 521 // match only on /+1 character minimum
517 522 var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine);
518 523
519 524 var tokenStr = '';
520 525 if (tokenMatch !== null && tokenMatch.length > 0){
521 526 tokenStr = tokenMatch[2].strip();
522 527 }
523 528
524 529 var context = {
525 530 start: (cur.ch - tokenStr.length) - 1,
526 531 end: cur.ch,
527 532 string: tokenStr,
528 533 type: null
529 534 };
530 535
531 536 return {
532 537 list: filterActions(options.registeredActions, context),
533 538 from: CodeMirror.Pos(cur.line, context.start),
534 539 to: CodeMirror.Pos(cur.line, context.end)
535 540 };
536 541
537 542 };
538 543 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
539 544 CodeMirror.registerHelper("hint", "actions", actionHint);
540 545 return cm;
541 546 };
542 547
543 548 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
544 549 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
545 550 codeMirrorInstance.setOption("mode", mode);
546 551 };
547 552
548 553 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
549 554 codeMirrorInstance.setOption("lineWrapping", line_wrap);
550 555 };
551 556
552 557 var setCodeMirrorModeFromSelect = function(
553 558 targetSelect, targetFileInput, codeMirrorInstance, callback){
554 559
555 560 $(targetSelect).on('change', function(e) {
556 561 cmLog.debug('codemirror select2 mode change event !');
557 562 var selected = e.currentTarget;
558 563 var node = selected.options[selected.selectedIndex];
559 564 var mimetype = node.value;
560 565 cmLog.debug('picked mimetype', mimetype);
561 566 var new_mode = $(node).attr('mode');
562 567 setCodeMirrorMode(codeMirrorInstance, new_mode);
563 568 cmLog.debug('set new mode', new_mode);
564 569
565 570 //propose filename from picked mode
566 571 cmLog.debug('setting mimetype', mimetype);
567 572 var proposed_ext = getExtFromMimeType(mimetype);
568 573 cmLog.debug('file input', $(targetFileInput).val());
569 574 var file_data = getFilenameAndExt($(targetFileInput).val());
570 575 var filename = file_data.filename || 'filename1';
571 576 $(targetFileInput).val(filename + proposed_ext);
572 577 cmLog.debug('proposed file', filename + proposed_ext);
573 578
574 579
575 580 if (typeof(callback) === 'function') {
576 581 try {
577 582 cmLog.debug('running callback', callback);
578 583 callback(filename, mimetype, new_mode);
579 584 } catch (err) {
580 585 console.log('failed to run callback', callback, err);
581 586 }
582 587 }
583 588 cmLog.debug('finish iteration...');
584 589 });
585 590 };
586 591
587 592 var setCodeMirrorModeFromInput = function(
588 593 targetSelect, targetFileInput, codeMirrorInstance, callback) {
589 594
590 595 // on type the new filename set mode
591 596 $(targetFileInput).on('keyup', function(e) {
592 597 var file_data = getFilenameAndExt(this.value);
593 598 if (file_data.ext === null) {
594 599 return;
595 600 }
596 601
597 602 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
598 603 cmLog.debug('mimetype from file', file_data, mimetypes);
599 604 var detected_mode;
600 605 var detected_option;
601 606 for (var i in mimetypes) {
602 607 var mt = mimetypes[i];
603 608 if (!detected_mode) {
604 609 detected_mode = detectCodeMirrorMode(this.value, mt);
605 610 }
606 611
607 612 if (!detected_option) {
608 613 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
609 614 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
610 615 detected_option = mt;
611 616 }
612 617 }
613 618 }
614 619
615 620 cmLog.debug('detected mode', detected_mode);
616 621 cmLog.debug('detected option', detected_option);
617 622 if (detected_mode && detected_option){
618 623
619 624 $(targetSelect).select2("val", detected_option);
620 625 setCodeMirrorMode(codeMirrorInstance, detected_mode);
621 626
622 627 if(typeof(callback) === 'function'){
623 628 try{
624 629 cmLog.debug('running callback', callback);
625 630 var filename = file_data.filename + "." + file_data.ext;
626 631 callback(filename, detected_option, detected_mode);
627 632 }catch (err){
628 633 console.log('failed to run callback', callback, err);
629 634 }
630 635 }
631 636 }
632 637
633 638 });
634 639 };
635 640
636 641 var fillCodeMirrorOptions = function(targetSelect) {
637 642 //inject new modes, based on codeMirrors modeInfo object
638 643 var modes_select = $(targetSelect);
639 644 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
640 645 var m = CodeMirror.modeInfo[i];
641 646 var opt = new Option(m.name, m.mime);
642 647 $(opt).attr('mode', m.mode);
643 648 modes_select.append(opt);
644 649 }
645 650 };
646 651
647 var CodeMirrorPreviewEnable = function(edit_mode) {
648 // in case it a preview enabled mode enable the button
649 if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) {
650 $('#render_preview').removeClass('hidden');
651 }
652 else {
653 if (!$('#render_preview').hasClass('hidden')) {
654 $('#render_preview').addClass('hidden');
655 }
656 }
657 };
658
659 652
660 653 /* markup form */
661 654 (function(mod) {
662 655
663 656 if (typeof exports == "object" && typeof module == "object") {
664 657 // CommonJS
665 658 module.exports = mod();
666 659 }
667 660 else {
668 661 // Plain browser env
669 662 (this || window).MarkupForm = mod();
670 663 }
671 664
672 665 })(function() {
673 666 "use strict";
674 667
675 668 function MarkupForm(textareaId) {
676 669 if (!(this instanceof MarkupForm)) {
677 670 return new MarkupForm(textareaId);
678 671 }
679 672
680 673 // bind the element instance to our Form
681 674 $('#' + textareaId).get(0).MarkupForm = this;
682 675
683 676 this.withSelectorId = function(selector) {
684 677 var selectorId = textareaId;
685 678 return selector + '_' + selectorId;
686 679 };
687 680
688 681 this.previewButton = this.withSelectorId('#preview-btn');
689 682 this.previewContainer = this.withSelectorId('#preview-container');
690 683
691 684 this.previewBoxSelector = this.withSelectorId('#preview-box');
692 685
693 686 this.editButton = this.withSelectorId('#edit-btn');
694 687 this.editContainer = this.withSelectorId('#edit-container');
695 688
696 689 this.cmBox = textareaId;
697 690 this.cm = initMarkupCodeMirror('#' + textareaId);
698 691
699 692 this.previewUrl = pyroutes.url('markup_preview');
700 693
701 694 // FUNCTIONS and helpers
702 695 var self = this;
703 696
704 697 this.getCmInstance = function(){
705 698 return this.cm
706 699 };
707 700
708 701 this.setPlaceholder = function(placeholder) {
709 702 var cm = this.getCmInstance();
710 703 if (cm){
711 704 cm.setOption('placeholder', placeholder);
712 705 }
713 706 };
714 707
715 708 this.initStatusChangeSelector = function(){
716 709 var formatChangeStatus = function(state, escapeMarkup) {
717 710 var originalOption = state.element;
718 711 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
719 712 '<span>' + escapeMarkup(state.text) + '</span>';
720 713 };
721 714 var formatResult = function(result, container, query, escapeMarkup) {
722 715 return formatChangeStatus(result, escapeMarkup);
723 716 };
724 717
725 718 var formatSelection = function(data, container, escapeMarkup) {
726 719 return formatChangeStatus(data, escapeMarkup);
727 720 };
728 721
729 722 $(this.submitForm).find(this.statusChange).select2({
730 723 placeholder: _gettext('Status Review'),
731 724 formatResult: formatResult,
732 725 formatSelection: formatSelection,
733 726 containerCssClass: "drop-menu status_box_menu",
734 727 dropdownCssClass: "drop-menu-dropdown",
735 728 dropdownAutoWidth: true,
736 729 minimumResultsForSearch: -1
737 730 });
738 731 $(this.submitForm).find(this.statusChange).on('change', function() {
739 732 var status = self.getCommentStatus();
740 733
741 734 if (status && !self.isInline()) {
742 735 $(self.submitButton).prop('disabled', false);
743 736 }
744 737
745 738 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
746 739 self.setPlaceholder(placeholderText)
747 740 })
748 741 };
749 742
750 743 // reset the text area into it's original state
751 744 this.resetMarkupFormState = function(content) {
752 745 content = content || '';
753 746
754 747 $(this.editContainer).show();
755 748 $(this.editButton).parent().addClass('active');
756 749
757 750 $(this.previewContainer).hide();
758 751 $(this.previewButton).parent().removeClass('active');
759 752
760 753 this.setActionButtonsDisabled(true);
761 754 self.cm.setValue(content);
762 755 self.cm.setOption("readOnly", false);
763 756 };
764 757
765 758 this.previewSuccessCallback = function(o) {
766 759 $(self.previewBoxSelector).html(o);
767 760 $(self.previewBoxSelector).removeClass('unloaded');
768 761
769 762 // swap buttons, making preview active
770 763 $(self.previewButton).parent().addClass('active');
771 764 $(self.editButton).parent().removeClass('active');
772 765
773 766 // unlock buttons
774 767 self.setActionButtonsDisabled(false);
775 768 };
776 769
777 770 this.setActionButtonsDisabled = function(state) {
778 771 $(this.editButton).prop('disabled', state);
779 772 $(this.previewButton).prop('disabled', state);
780 773 };
781 774
782 775 // lock preview/edit/submit buttons on load, but exclude cancel button
783 776 var excludeCancelBtn = true;
784 777 this.setActionButtonsDisabled(true);
785 778
786 779 // anonymous users don't have access to initialized CM instance
787 780 if (this.cm !== undefined){
788 781 this.cm.on('change', function(cMirror) {
789 782 if (cMirror.getValue() === "") {
790 783 self.setActionButtonsDisabled(true)
791 784 } else {
792 785 self.setActionButtonsDisabled(false)
793 786 }
794 787 });
795 788 }
796 789
797 790 $(this.editButton).on('click', function(e) {
798 791 e.preventDefault();
799 792
800 793 $(self.previewButton).parent().removeClass('active');
801 794 $(self.previewContainer).hide();
802 795
803 796 $(self.editButton).parent().addClass('active');
804 797 $(self.editContainer).show();
805 798
806 799 });
807 800
808 801 $(this.previewButton).on('click', function(e) {
809 802 e.preventDefault();
810 803 var text = self.cm.getValue();
811 804
812 805 if (text === "") {
813 806 return;
814 807 }
815 808
816 809 var postData = {
817 810 'text': text,
818 811 'renderer': templateContext.visual.default_renderer,
819 812 'csrf_token': CSRF_TOKEN
820 813 };
821 814
822 815 // lock ALL buttons on preview
823 816 self.setActionButtonsDisabled(true);
824 817
825 818 $(self.previewBoxSelector).addClass('unloaded');
826 819 $(self.previewBoxSelector).html(_gettext('Loading ...'));
827 820
828 821 $(self.editContainer).hide();
829 822 $(self.previewContainer).show();
830 823
831 824 // by default we reset state of comment preserving the text
832 825 var previewFailCallback = function(data){
833 826 alert(
834 827 "Error while submitting preview.\n" +
835 828 "Error code {0} ({1}).".format(data.status, data.statusText)
836 829 );
837 830 self.resetMarkupFormState(text)
838 831 };
839 832 _submitAjaxPOST(
840 833 self.previewUrl, postData, self.previewSuccessCallback,
841 834 previewFailCallback);
842 835
843 836 $(self.previewButton).parent().addClass('active');
844 837 $(self.editButton).parent().removeClass('active');
845 838 });
846 839
847 840 }
848 841
849 842 return MarkupForm;
850 843 });
@@ -1,411 +1,517 b''
1 1 // # Copyright (C) 2010-2019 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 * Search file list
21 21 */
22 22
23 23 var NodeFilter = {};
24 24
25 25 var fileBrowserListeners = function (node_list_url, url_base) {
26 26 var $filterInput = $('#node_filter');
27 27 var n_filter = $filterInput.get(0);
28 28
29 29 NodeFilter.filterTimeout = null;
30 30 var nodes = null;
31 31
32 32 NodeFilter.focus = function () {
33 33 $filterInput.focus()
34 34 };
35 35
36 36 NodeFilter.fetchNodes = function (callback) {
37 37 $.ajax(
38 38 {url: node_list_url, headers: {'X-PARTIAL-XHR': true}})
39 39 .done(function (data) {
40 40 nodes = data.nodes;
41 41 if (callback) {
42 42 callback();
43 43 }
44 44 })
45 45 .fail(function (data) {
46 46 console.log('failed to load');
47 47 });
48 48 };
49 49
50 50 NodeFilter.initFilter = function (e) {
51 51 if ($filterInput.hasClass('loading')) {
52 52 return
53 53 }
54 54
55 55 // in case we are already loaded, do nothing
56 56 if (!$filterInput.hasClass('init')) {
57 57 return NodeFilter.handleKey(e);
58 58 }
59 59 var iconLoading = 'icon-spin animate-spin';
60 60 var iconSearch = 'icon-search';
61 61 $('.files-filter-box-path i').removeClass(iconSearch).addClass(iconLoading);
62 62 $filterInput.addClass('loading');
63 63
64 64 var callback = function (org) {
65 65 return function () {
66 66 if ($filterInput.hasClass('init')) {
67 67 $filterInput.removeClass('init');
68 68 $filterInput.removeClass('loading');
69 69 }
70 70 $('.files-filter-box-path i').removeClass(iconLoading).addClass(iconSearch);
71 71
72 72 // auto re-filter if we filled in the input
73 73 if (n_filter.value !== "") {
74 74 NodeFilter.updateFilter(n_filter, e)()
75 75 }
76 76
77 77 }
78 78 };
79 79 // load node data
80 80 NodeFilter.fetchNodes(callback());
81 81
82 82 };
83 83
84 84 NodeFilter.resetFilter = function () {
85 85 $('#tbody').show();
86 86 $('#tbody_filtered').hide();
87 87 $filterInput.val('');
88 88 };
89 89
90 90 NodeFilter.handleKey = function (e) {
91 91 var scrollDown = function (element) {
92 92 var elementBottom = element.offset().top + $(element).outerHeight();
93 93 var windowBottom = window.innerHeight + $(window).scrollTop();
94 94 if (elementBottom > windowBottom) {
95 95 var offset = elementBottom - window.innerHeight;
96 96 $('html,body').scrollTop(offset);
97 97 return false;
98 98 }
99 99 return true;
100 100 };
101 101
102 102 var scrollUp = function (element) {
103 103 if (element.offset().top < $(window).scrollTop()) {
104 104 $('html,body').scrollTop(element.offset().top);
105 105 return false;
106 106 }
107 107 return true;
108 108 };
109 109 var $hlElem = $('.browser-highlight');
110 110
111 111 if (e.keyCode === 40) { // Down
112 112 if ($hlElem.length === 0) {
113 113 $('.browser-result').first().addClass('browser-highlight');
114 114 } else {
115 115 var next = $hlElem.next();
116 116 if (next.length !== 0) {
117 117 $hlElem.removeClass('browser-highlight');
118 118 next.addClass('browser-highlight');
119 119 }
120 120 }
121 121
122 122 if ($hlElem.get(0) !== undefined){
123 123 scrollDown($hlElem);
124 124 }
125 125 }
126 126 if (e.keyCode === 38) { // Up
127 127 e.preventDefault();
128 128 if ($hlElem.length !== 0) {
129 129 var next = $hlElem.prev();
130 130 if (next.length !== 0) {
131 131 $('.browser-highlight').removeClass('browser-highlight');
132 132 next.addClass('browser-highlight');
133 133 }
134 134 }
135 135
136 136 if ($hlElem.get(0) !== undefined){
137 137 scrollUp($hlElem);
138 138 }
139 139
140 140 }
141 141 if (e.keyCode === 13) { // Enter
142 142 if ($('.browser-highlight').length !== 0) {
143 143 var url = $('.browser-highlight').find('.match-link').attr('href');
144 144 window.location = url;
145 145 }
146 146 }
147 147 if (e.keyCode === 27) { // Esc
148 148 NodeFilter.resetFilter();
149 149 $('html,body').scrollTop(0);
150 150 }
151 151
152 152 var capture_keys = [
153 153 40, // ArrowDown
154 154 38, // ArrowUp
155 155 39, // ArrowRight
156 156 37, // ArrowLeft
157 157 13, // Enter
158 158 27 // Esc
159 159 ];
160 160
161 161 if ($.inArray(e.keyCode, capture_keys) === -1) {
162 162 clearTimeout(NodeFilter.filterTimeout);
163 163 NodeFilter.filterTimeout = setTimeout(NodeFilter.updateFilter(n_filter, e), 200);
164 164 }
165 165
166 166 };
167 167
168 168 NodeFilter.fuzzy_match = function (filepath, query) {
169 169 var highlight = [];
170 170 var order = 0;
171 171 for (var i = 0; i < query.length; i++) {
172 172 var match_position = filepath.indexOf(query[i]);
173 173 if (match_position !== -1) {
174 174 var prev_match_position = highlight[highlight.length - 1];
175 175 if (prev_match_position === undefined) {
176 176 highlight.push(match_position);
177 177 } else {
178 178 var current_match_position = prev_match_position + match_position + 1;
179 179 highlight.push(current_match_position);
180 180 order = order + current_match_position - prev_match_position;
181 181 }
182 182 filepath = filepath.substring(match_position + 1);
183 183 } else {
184 184 return false;
185 185 }
186 186 }
187 187 return {
188 188 'order': order,
189 189 'highlight': highlight
190 190 };
191 191 };
192 192
193 193 NodeFilter.sortPredicate = function (a, b) {
194 194 if (a.order < b.order) return -1;
195 195 if (a.order > b.order) return 1;
196 196 if (a.filepath < b.filepath) return -1;
197 197 if (a.filepath > b.filepath) return 1;
198 198 return 0;
199 199 };
200 200
201 201 NodeFilter.updateFilter = function (elem, e) {
202 202 return function () {
203 203 // Reset timeout
204 204 NodeFilter.filterTimeout = null;
205 205 var query = elem.value.toLowerCase();
206 206 var match = [];
207 207 var matches_max = 20;
208 208 if (query !== "") {
209 209 var results = [];
210 210 for (var k = 0; k < nodes.length; k++) {
211 211 var result = NodeFilter.fuzzy_match(
212 212 nodes[k].name.toLowerCase(), query);
213 213 if (result) {
214 214 result.type = nodes[k].type;
215 215 result.filepath = nodes[k].name;
216 216 results.push(result);
217 217 }
218 218 }
219 219 results = results.sort(NodeFilter.sortPredicate);
220 220 var limit = matches_max;
221 221 if (results.length < matches_max) {
222 222 limit = results.length;
223 223 }
224 224 for (var i = 0; i < limit; i++) {
225 225 if (query && results.length > 0) {
226 226 var n = results[i].filepath;
227 227 var t = results[i].type;
228 228 var n_hl = n.split("");
229 229 var pos = results[i].highlight;
230 230 for (var j = 0; j < pos.length; j++) {
231 231 n_hl[pos[j]] = "<em>" + n_hl[pos[j]] + "</em>";
232 232 }
233 233 n_hl = n_hl.join("");
234 234 var new_url = url_base.replace('__FPATH__', n);
235 235
236 236 var typeObj = {
237 237 dir: 'icon-directory browser-dir',
238 238 file: 'icon-file-text browser-file'
239 239 };
240 240
241 241 var typeIcon = '<i class="{0}"></i>'.format(typeObj[t]);
242 242 match.push('<tr class="browser-result"><td><a class="match-link" href="{0}">{1}{2}</a></td><td colspan="5"></td></tr>'.format(new_url, typeIcon, n_hl));
243 243 }
244 244 }
245 245 if (results.length > limit) {
246 246 var truncated_count = results.length - matches_max;
247 247 if (truncated_count === 1) {
248 248 match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated result')));
249 249 } else {
250 250 match.push('<tr><td>{0} {1}</td><td colspan="5"></td></tr>'.format(truncated_count, _gettext('truncated results')));
251 251 }
252 252 }
253 253 }
254 254 if (query !== "") {
255 255 $('#tbody').hide();
256 256 $('#tbody_filtered').show();
257 257
258 258 if (match.length === 0) {
259 259 match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_gettext('No matching files')));
260 260 }
261 261 $('#tbody_filtered').html(match.join(""));
262 262 } else {
263 263 $('#tbody').show();
264 264 $('#tbody_filtered').hide();
265 265 }
266 266
267 267 };
268 268 };
269 269
270 270 };
271 271
272 272 var getIdentNode = function(n){
273 273 // iterate through nodes until matched interesting node
274 274 if (typeof n === 'undefined'){
275 275 return -1;
276 276 }
277 277 if(typeof n.id !== "undefined" && n.id.match('L[0-9]+')){
278 278 return n;
279 279 }
280 280 else{
281 281 return getIdentNode(n.parentNode);
282 282 }
283 283 };
284 284
285 285 var getSelectionLink = function(e) {
286 286 // get selection from start/to nodes
287 287 if (typeof window.getSelection !== "undefined") {
288 288 s = window.getSelection();
289 289
290 290 from = getIdentNode(s.anchorNode);
291 291 till = getIdentNode(s.focusNode);
292 292
293 293 f_int = parseInt(from.id.replace('L',''));
294 294 t_int = parseInt(till.id.replace('L',''));
295 295
296 296 if (f_int > t_int){
297 297 // highlight from bottom
298 298 offset = -35;
299 299 ranges = [t_int,f_int];
300 300 }
301 301 else{
302 302 // highligth from top
303 303 offset = 35;
304 304 ranges = [f_int,t_int];
305 305 }
306 306 // if we select more than 2 lines
307 307 if (ranges[0] !== ranges[1]){
308 308 if($('#linktt').length === 0){
309 309 hl_div = document.createElement('div');
310 310 hl_div.id = 'linktt';
311 311 }
312 312 hl_div.innerHTML = '';
313 313
314 314 anchor = '#L'+ranges[0]+'-'+ranges[1];
315 315 var link = document.createElement('a');
316 316 link.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
317 317 link.innerHTML = _gettext('Selection link');
318 318 hl_div.appendChild(link);
319 319 $('#codeblock').append(hl_div);
320 320
321 321 var xy = $(till).offset();
322 322 $('#linktt').addClass('hl-tip-box tip-box');
323 323 $('#linktt').offset({top: xy.top + offset, left: xy.left});
324 324 $('#linktt').css('visibility','visible');
325 325 }
326 326 else{
327 327 $('#linktt').css('visibility','hidden');
328 328 }
329 329 }
330 330 };
331 331
332 332 var getFileState = function() {
333 333 // relies on a global set filesUrlData
334 334 var f_path = filesUrlData['f_path'];
335 335 var commit_id = filesUrlData['commit_id'];
336 336
337 337 var url_params = {
338 338 repo_name: templateContext.repo_name,
339 339 commit_id: commit_id,
340 340 f_path:'__FPATH__'
341 341 };
342 342 if (atRef !== '') {
343 343 url_params['at'] = atRef
344 344 }
345 345
346 346 var _url_base = pyroutes.url('repo_files', url_params);
347 347 var _node_list_url = pyroutes.url('repo_files_nodelist',
348 348 {repo_name: templateContext.repo_name,
349 349 commit_id: commit_id, f_path: f_path});
350 350
351 351 return {
352 352 f_path: f_path,
353 353 commit_id: commit_id,
354 354 node_list_url: _node_list_url,
355 355 url_base: _url_base
356 356 };
357 357 };
358 358
359 359 var getFilesMetadata = function() {
360 360 // relies on metadataRequest global state
361 361 if (metadataRequest && metadataRequest.readyState != 4) {
362 362 metadataRequest.abort();
363 363 }
364 364
365 365 if ($('#file-tree-wrapper').hasClass('full-load')) {
366 366 // in case our HTML wrapper has full-load class we don't
367 367 // trigger the async load of metadata
368 368 return false;
369 369 }
370 370
371 371 var state = getFileState();
372 372 var url_data = {
373 373 'repo_name': templateContext.repo_name,
374 374 'commit_id': state.commit_id,
375 375 'f_path': state.f_path
376 376 };
377 377
378 378 var url = pyroutes.url('repo_nodetree_full', url_data);
379 379
380 380 metadataRequest = $.ajax({url: url});
381 381
382 382 metadataRequest.done(function(data) {
383 383 $('#file-tree').html(data);
384 384 timeagoActivate();
385 385 });
386 386 metadataRequest.fail(function (data, textStatus, errorThrown) {
387 387 if (data.status != 0) {
388 388 alert("Error while fetching metadata.\nError code {0} ({1}).Please consider reloading the page".format(data.status,data.statusText));
389 389 }
390 390 });
391 391 };
392 392
393 393 // show more authors
394 394 var showAuthors = function(elem, annotate) {
395 395 var state = getFileState('callbacks');
396 396
397 397 var url = pyroutes.url('repo_file_authors',
398 398 {'repo_name': templateContext.repo_name,
399 399 'commit_id': state.commit_id, 'f_path': state.f_path});
400 400
401 401 $.pjax({
402 402 url: url,
403 403 data: 'annotate={0}'.format(annotate),
404 404 container: '#file_authors',
405 405 push: false,
406 406 timeout: 5000
407 407 }).complete(function(){
408 408 $(elem).hide();
409 409 $('#file_authors_title').html(_gettext('All Authors'))
410 410 })
411 411 };
412
413
414 (function (mod) {
415
416 if (typeof exports == "object" && typeof module == "object") {
417 // CommonJS
418 module.exports = mod();
419 } else {
420 // Plain browser env
421 (this || window).FileEditor = mod();
422 }
423
424 })(function () {
425 "use strict";
426
427 function FileEditor(textAreaElement, options) {
428 if (!(this instanceof FileEditor)) {
429 return new FileEditor(textAreaElement, options);
430 }
431 // bind the element instance to our Form
432 var te = $(textAreaElement).get(0);
433 if (te !== undefined) {
434 te.FileEditor = this;
435 }
436
437 this.modes_select = '#set_mode';
438 this.filename_selector = '#filename';
439 this.commit_btn_selector = '#commit_btn';
440 this.line_wrap_selector = '#line_wrap';
441 this.editor_preview_selector = '#editor_preview';
442
443 if (te !== undefined) {
444 this.cm = initCodeMirror(textAreaElement, null, false);
445 }
446
447 // FUNCTIONS and helpers
448 var self = this;
449
450 this.submitHandler = function() {
451 $(self.commit_btn_selector).on('click', function(e) {
452
453 var filename = $(self.filename_selector).val();
454 if (filename === "") {
455 alert("Missing filename");
456 e.preventDefault();
457 }
458
459 var button = $(this);
460 if (button.hasClass('clicked')) {
461 button.attr('disabled', true);
462 } else {
463 button.addClass('clicked');
464 }
465 });
466 };
467 this.submitHandler();
468
469 // on select line wraps change the editor
470 this.lineWrapHandler = function () {
471 $(self.line_wrap_selector).on('change', function (e) {
472 var selected = e.currentTarget;
473 var line_wraps = {'on': true, 'off': false}[selected.value];
474 setCodeMirrorLineWrap(self.cm, line_wraps)
475 });
476 };
477 this.lineWrapHandler();
478
479
480 this.showPreview = function () {
481
482 var _text = self.cm.getValue();
483 var _file_path = $(self.filename_selector).val();
484 if (_text && _file_path) {
485 $('.show-preview').addClass('active');
486 $('.show-editor').removeClass('active');
487
488 $(self.editor_preview_selector).show();
489 $(self.cm.getWrapperElement()).hide();
490
491
492 var post_data = {'text': _text, 'file_path': _file_path, 'csrf_token': CSRF_TOKEN};
493 $(self.editor_preview_selector).html(_gettext('Loading ...'));
494
495 var url = pyroutes.url('file_preview');
496
497 ajaxPOST(url, post_data, function (o) {
498 $(self.editor_preview_selector).html(o);
499 })
500 }
501
502 };
503
504 this.showEditor = function () {
505 $(self.editor_preview_selector).hide();
506 $('.show-editor').addClass('active');
507 $('.show-preview').removeClass('active');
508
509 $(self.cm.getWrapperElement()).show();
510 };
511
512
513 }
514
515 return FileEditor;
516 });
517
@@ -1,182 +1,110 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 ${_('%s Files Add') % c.repo_name}
4 ${_('{} Files Add').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 <%def name="breadcrumbs_links()">
15 ${_('Add new file')} @ ${h.show_id(c.commit)} ${_('Branch')}: ${c.commit.branch}
16 </%def>
14 <%def name="breadcrumbs_links()"></%def>
17 15
18 16 <%def name="menu_bar_subnav()">
19 17 ${self.repo_menu(active='files')}
20 18 </%def>
21 19
22 20 <%def name="main()">
21
23 22 <div class="box">
24 23
25 24 <div class="edit-file-title">
26 ${self.breadcrumbs()}
25 <span class="title-heading">${_('Add new file')} @ <code>${h.show_id(c.commit)}</code></span>
26 <span class="tag branchtag"><i class="icon-branch"></i> ${c.commit.branch}</span>
27 27 </div>
28 28
29 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', enctype="multipart/form-data", class_="form-horizontal", request=request)}
29 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
30 30 <div class="edit-file-fieldset">
31 <div class="fieldset">
32 <div id="destination-label" class="left-label">
33 ${_('Path')}:
34 </div>
35 <div class="right-content">
31 <div class="path-items">
32 <ul>
33 <li class="breadcrumb-path">
36 34 <div>
37 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))} /
38 <input type="input-small" value="${c.f_path}" size="46" name="location" id="location">
35 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
36 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a> ${('/' if c.f_path else '')}
39 37 </div>
40 </div>
41 </div>
42 <div id="filename_container" class="fieldset">
43 <div class="filename-label left-label">
44 ${_('Filename')}:
45 </div>
46 <div class="right-content">
47 <input class="input-small" type="text" value="" size="46" name="filename" id="filename">
48
49 </div>
38 </li>
39 <li class="location-path">
40 <input class="file-name-input input-small" type="text" value="" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
41 </li>
42 </ul>
50 43 </div>
51 44
52 45 </div>
53 46
54 47 <div class="table">
55 48 <div id="files_data">
49
56 50 <div id="codeblock" class="codeblock">
57 <div class="code-header form" id="set_mode_header">
58 <div class="fields">
59 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
60 <label for="line_wrap">${_('line wraps')}</label>
61 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
51 <div class="editor-items">
52 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
53 ${_('Edit')}
54 </div>
55
56 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
57 ${_('Preview')}
58 </div>
62 59
63 <div id="render_preview" class="btn btn-small preview hidden" >${_('Preview')}</div>
60 <div class="pull-right">
61 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off'))], extra_classes=['last-item'])}
62 </div>
63 <div class="pull-right">
64 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))], enable_filter=True)}
65 </div>
64 66 </div>
65 </div>
67
66 68 <div id="editor_container">
67 69 <pre id="editor_pre"></pre>
68 70 <textarea id="editor" name="content" ></textarea>
69 71 <div id="editor_preview"></div>
70 72 </div>
71 73 </div>
72 74 </div>
73 75 </div>
74 76
75 77 <div class="edit-file-fieldset">
76 78 <div class="fieldset">
77 <div id="commit-message-label" class="commit-message-label left-label">
78 ${_('Commit Message')}:
79 </div>
80 <div class="right-content">
81 <div class="message">
82 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
83 </div>
79 <div class="message">
80 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
84 81 </div>
85 82 </div>
86 <div class="pull-right">
87 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
88 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-success")}
83 <div class="pull-left">
84 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
89 85 </div>
90 86 </div>
91 87 ${h.end_form()}
92 88 </div>
89
93 90 <script type="text/javascript">
94 91
95 $('#commit_btn').on('click', function() {
96 var button = $(this);
97 if (button.hasClass('clicked')) {
98 button.attr('disabled', true);
99 } else {
100 button.addClass('clicked');
101 }
102 });
103
104 var hide_upload = function(){
105 $('#files_data').show();
106 $('#upload_file_container').hide();
107 $('#filename_container').show();
108 };
92 $(document).ready(function () {
93 var modes_select = $('#set_mode');
94 var filename_selector = '#filename';
95 fillCodeMirrorOptions(modes_select);
109 96
110 $('#file_enable').on('click', function(e){
111 e.preventDefault();
112 hide_upload();
113 });
114
115 var renderer = "";
116 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}";
117 var myCodeMirror = initCodeMirror('editor', reset_url, false);
118
119 var modes_select = $('#set_mode');
120 fillCodeMirrorOptions(modes_select);
121
122 var filename_selector = '#filename';
123 var callback = function(filename, mimetype, mode){
124 CodeMirrorPreviewEnable(mode);
125 };
126 // on change of select field set mode
127 setCodeMirrorModeFromSelect(
128 modes_select, filename_selector, myCodeMirror, callback);
129
130 // on entering the new filename set mode, from given extension
131 setCodeMirrorModeFromInput(
132 modes_select, filename_selector, myCodeMirror, callback);
97 fileEditor = new FileEditor('#editor');
133 98
134 // if the file is renderable set line wraps automatically
135 if (renderer !== ""){
136 var line_wrap = 'on';
137 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
138 setCodeMirrorLineWrap(myCodeMirror, true);
139 }
140
141 // on select line wraps change the editor
142 $('#line_wrap').on('change', function(e){
143 var selected = e.currentTarget;
144 var line_wraps = {'on': true, 'off': false}[selected.value];
145 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
146 });
147
148 // render preview/edit button
149 $('#render_preview').on('click', function(e){
150 if($(this).hasClass('preview')){
151 $(this).removeClass('preview');
152 $(this).html("${_('Edit')}");
153 $('#editor_preview').show();
154 $(myCodeMirror.getWrapperElement()).hide();
99 // on change of select field set mode
100 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
155 101
156 var possible_renderer = {
157 'rst':'rst',
158 'markdown':'markdown',
159 'gfm': 'markdown'}[myCodeMirror.getMode().name];
160 var _text = myCodeMirror.getValue();
161 var _renderer = possible_renderer || DEFAULT_RENDERER;
162 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
163 $('#editor_preview').html(_gettext('Loading ...'));
164 var url = pyroutes.url('repo_commit_comment_preview',
165 {'repo_name': '${c.repo_name}',
166 'commit_id': '${c.commit.raw_id}'});
102 // on entering the new filename set mode, from given extension
103 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
167 104
168 ajaxPOST(url, post_data, function(o){
169 $('#editor_preview').html(o);
170 })
171 }
172 else{
173 $(this).addClass('preview');
174 $(this).html("${_('Preview')}");
175 $('#editor_preview').hide();
176 $(myCodeMirror.getWrapperElement()).show();
177 }
105 $('#filename').focus();
106
178 107 });
179 $('#filename').focus();
180 108
181 109 </script>
182 110 </%def>
@@ -1,72 +1,85 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 ${_('%s Files Delete') % c.repo_name}
4 ${_('{} Files Delete').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 <%def name="breadcrumbs_links()">
15 ${_('Delete file')} @ ${h.show_id(c.commit)}
16 </%def>
14 <%def name="breadcrumbs_links()"></%def>
17 15
18 16 <%def name="menu_bar_subnav()">
19 17 ${self.repo_menu(active='files')}
20 18 </%def>
21 19
22 20 <%def name="main()">
21
23 22 <div class="box">
23
24 24 <div class="edit-file-title">
25 ${self.breadcrumbs()}
25 <span class="title-heading">${_('Delete file')} @ <code>${h.show_id(c.commit)}</code></span>
26 <span class="tag branchtag"><i class="icon-branch"></i> ${c.commit.branch}</span>
26 27 </div>
27 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', class_="form-horizontal", request=request)}
28
29 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
28 30 <div class="edit-file-fieldset">
29 <div class="fieldset">
30 <div id="destination-label" class="left-label">
31 ${_('Path')}:
32 </div>
33 <div class="right-content">
34 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))}</span>
35 </div>
31 <div class="path-items">
32 <li class="breadcrumb-path">
33 <div>
34 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
35 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.file.dir_path)}">${c.file.dir_path}</a> ${('/' if c.file.dir_path else '')}
36 </div>
37 </li>
38 <li class="location-path">
39 <input type="hidden" value="${c.f_path}" name="root_path">
40 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" disabled="disabled">
41 </li>
36 42 </div>
43
37 44 </div>
38 45
39 46 <div id="codeblock" class="codeblock delete-file-preview">
40 47 <div class="code-body">
41 48 %if c.file.is_binary:
42 49 ${_('Binary file (%s)') % c.file.mimetype}
43 50 %else:
44 51 %if c.file.size < c.visual.cut_off_limit_file:
45 52 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
46 53 %else:
47 54 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
48 55 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
49 56 %endif
50 57 %endif
51 58 </div>
52 59 </div>
53 60
54 61 <div class="edit-file-fieldset">
55 62 <div class="fieldset">
56 <div id="commit-message-label" class="commit-message-label left-label">
57 ${_('Commit Message')}:
58 </div>
59 <div class="right-content">
60 <div class="message">
61 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
62 </div>
63 <div class="message">
64 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
63 65 </div>
64 66 </div>
65 <div class="pull-right">
66 ${h.reset('reset',_('Cancel'),class_="btn btn-small btn-danger")}
67 ${h.submit('commit',_('Delete File'),class_="btn btn-small btn-danger-action")}
67 <div class="pull-left">
68 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-danger-action")}
68 69 </div>
69 70 </div>
70 71 ${h.end_form()}
71 72 </div>
73
74
75 <script type="text/javascript">
76
77 $(document).ready(function () {
78
79 fileEditor = new FileEditor('#editor');
80
81 });
82
83 </script>
84
72 85 </%def>
@@ -1,194 +1,120 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 ${_('%s File Edit') % c.repo_name}
4 ${_('{} Files Edit').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 <%def name="breadcrumbs_links()">
15 ${_('Edit file')} @ ${h.show_id(c.commit)}
16 </%def>
14 <%def name="breadcrumbs_links()"></%def>
17 15
18 16 <%def name="menu_bar_subnav()">
19 17 ${self.repo_menu(active='files')}
20 18 </%def>
21 19
22 20 <%def name="main()">
23 <% renderer = h.renderer_from_filename(c.f_path)%>
21
24 22 <div class="box">
23
25 24 <div class="edit-file-title">
26 ${self.breadcrumbs()}
25 <span class="title-heading">${_('Edit file')} @ <code>${h.show_id(c.commit)}</code></span>
26 <span class="tag branchtag"><i class="icon-branch"></i> ${c.commit.branch}</span>
27 27 </div>
28
29 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
28 30 <div class="edit-file-fieldset">
29 <div class="fieldset">
30 <div id="destination-label" class="left-label">
31 ${_('Path')}:
32 </div>
33 <div class="right-content">
34 <div id="specify-custom-path-container">
35 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))}</span>
31 <div class="path-items">
32 <ul>
33 <li class="breadcrumb-path">
34 <div>
35 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
36 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.file.dir_path)}">${c.file.dir_path}</a> ${('/' if c.file.dir_path else '')}
36 37 </div>
37 </div>
38 </li>
39 <li class="location-path">
40 <input type="hidden" value="${c.f_path}" name="root_path">
41 <input class="file-name-input input-small" type="text" value="${c.file.name}" name="filename" id="filename" placeholder="${_('Filename e.g example.py, or docs/readme.md')}">
42 </li>
43 </ul>
38 44 </div>
45
39 46 </div>
40 47
41 48 <div class="table">
42 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', request=request)}
43 <div id="codeblock" class="codeblock" >
44 <div class="code-header">
45 <div class="stats">
46 <i class="icon-file"></i>
47 <span class="item">${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.file.commit.raw_id))}</span>
48 <span class="item">${h.format_byte_size_binary(c.file.size)}</span>
49 <span class="item last">${c.file.mimetype}</span>
50 <div class="buttons">
51 <a class="btn btn-mini" href="${h.route_path('repo_commits_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
52 <i class="icon-time"></i> ${_('history')}
53 </a>
49 <div id="files_data">
54 50
55 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
56 % if not c.file.is_binary:
57 %if True:
58 ${h.link_to(_('source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
59 %else:
60 ${h.link_to(_('annotation'),h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
61 %endif
51 <div id="codeblock" class="codeblock">
52 <div class="editor-items">
53 <div class="editor-action active show-editor pull-left" onclick="fileEditor.showEditor(); return false">
54 ${_('Edit')}
55 </div>
56
57 <div class="editor-action show-preview pull-left" onclick="fileEditor.showPreview(); return false">
58 ${_('Preview')}
59 </div>
62 60
63 <a class="btn btn-mini" href="${h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
64 ${_('raw')}
65 </a>
66 <a class="btn btn-mini" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
67 <i class="icon-archive"></i> ${_('download')}
68 </a>
69 % endif
70 % endif
61 <div class="pull-right">
62 ${h.dropdownmenu('line_wrap', 'off', [('on', _('Line wraps on')), ('off', _('line wraps off')),])}
63 </div>
64 <div class="pull-right">
65 ${h.dropdownmenu('set_mode','plain',[('plain', _('plain'))],enable_filter=True)}
66 </div>
71 67 </div>
72 </div>
73 <div class="form">
74 <label for="set_mode">${_('Editing file')}:</label>
75 ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path}
76 <input id="filename" type="text" name="filename" value="${c.file.name}">
77 68
78 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
79 <label for="line_wrap">${_('line wraps')}</label>
80 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
81
82 <div id="render_preview" class="btn btn-small preview hidden">${_('Preview')}</div>
83 </div>
84 </div>
85 <div id="editor_container">
86 <pre id="editor_pre"></pre>
87 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
88 <div id="editor_preview" ></div>
69 <div id="editor_container">
70 <pre id="editor_pre"></pre>
71 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
72 <div id="editor_preview" ></div>
73 </div>
89 74 </div>
90 75 </div>
91 76 </div>
92 77
93 78 <div class="edit-file-fieldset">
94 79 <div class="fieldset">
95 <div id="commit-message-label" class="commit-message-label left-label">
96 ${_('Commit Message')}:
97 </div>
98 <div class="right-content">
99 <div class="message">
100 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
101 </div>
80 <div class="message">
81 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
102 82 </div>
103 83 </div>
104 <div class="pull-right">
105 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
106 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")}
84 <div class="pull-left">
85 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
107 86 </div>
108 87 </div>
109 88 ${h.end_form()}
110 89 </div>
111 90
112 91 <script type="text/javascript">
113 $(document).ready(function(){
114 var renderer = "${renderer}";
115 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.path)}";
116 var myCodeMirror = initCodeMirror('editor', reset_url);
117 92
93 $(document).ready(function() {
118 94 var modes_select = $('#set_mode');
95 var filename_selector = '#filename';
119 96 fillCodeMirrorOptions(modes_select);
120 97
98 fileEditor = new FileEditor('#editor');
99
121 100 // try to detect the mode based on the file we edit
122 var mimetype = "${c.file.mimetype}";
123 var detected_mode = detectCodeMirrorMode(
124 "${c.file.name}", mimetype);
101 var detected_mode = detectCodeMirrorMode("${c.file.name}", "${c.file.mimetype}");
125 102
126 if(detected_mode){
127 setCodeMirrorMode(myCodeMirror, detected_mode);
128 $(modes_select).select2("val", mimetype);
129 $(modes_select).change();
130 setCodeMirrorMode(myCodeMirror, detected_mode);
103 if (detected_mode) {
104 setCodeMirrorMode(fileEditor.cm, detected_mode);
105
106 var mimetype = $(modes_select).find("option[mode={0}]".format(detected_mode)).val();
107 $(modes_select).select2("val", mimetype).trigger('change');
131 108 }
132 109
133 var filename_selector = '#filename';
134 var callback = function(filename, mimetype, mode){
135 CodeMirrorPreviewEnable(mode);
136 };
137 110 // on change of select field set mode
138 setCodeMirrorModeFromSelect(
139 modes_select, filename_selector, myCodeMirror, callback);
111 setCodeMirrorModeFromSelect(modes_select, filename_selector, fileEditor.cm, null);
140 112
141 113 // on entering the new filename set mode, from given extension
142 setCodeMirrorModeFromInput(
143 modes_select, filename_selector, myCodeMirror, callback);
144
145 // if the file is renderable set line wraps automatically
146 if (renderer !== ""){
147 var line_wrap = 'on';
148 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
149 setCodeMirrorLineWrap(myCodeMirror, true);
150 }
151 // on select line wraps change the editor
152 $('#line_wrap').on('change', function(e){
153 var selected = e.currentTarget;
154 var line_wraps = {'on': true, 'off': false}[selected.value];
155 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
156 });
114 setCodeMirrorModeFromInput(modes_select, filename_selector, fileEditor.cm, null);
157 115
158 // render preview/edit button
159 if (mimetype === 'text/x-rst' || mimetype === 'text/plain') {
160 $('#render_preview').removeClass('hidden');
161 }
162 $('#render_preview').on('click', function(e){
163 if($(this).hasClass('preview')){
164 $(this).removeClass('preview');
165 $(this).html("${_('Edit')}");
166 $('#editor_preview').show();
167 $(myCodeMirror.getWrapperElement()).hide();
116 });
168 117
169 var possible_renderer = {
170 'rst':'rst',
171 'markdown':'markdown',
172 'gfm': 'markdown'}[myCodeMirror.getMode().name];
173 var _text = myCodeMirror.getValue();
174 var _renderer = possible_renderer || DEFAULT_RENDERER;
175 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
176 $('#editor_preview').html(_gettext('Loading ...'));
177 var url = pyroutes.url('repo_commit_comment_preview',
178 {'repo_name': '${c.repo_name}',
179 'commit_id': '${c.commit.raw_id}'});
180 ajaxPOST(url, post_data, function(o){
181 $('#editor_preview').html(o);
182 })
183 }
184 else{
185 $(this).addClass('preview');
186 $(this).html("${_('Preview')}");
187 $('#editor_preview').hide();
188 $(myCodeMirror.getWrapperElement()).show();
189 }
190 });
191 118
192 })
193 119 </script>
194 120 </%def>
@@ -1,160 +1,160 b''
1 1 <%namespace name="sourceblock" file="/codeblocks/source.mako"/>
2 2
3 3 <div id="codeblock" class="browserblock">
4 4 <div class="browser-header">
5 5 <div class="browser-nav">
6 6 <div class="pull-left">
7 7 ## loads the history for a file
8 8 ${h.hidden('file_refs_filter')}
9 9 </div>
10 10
11 11 <div class="pull-right">
12 12
13 13 ## Download
14 14 % if c.lf_node:
15 15 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query=dict(lf=1))}">
16 16 ${_('Download largefile')}
17 17 </a>
18 18 % else:
19 19 <a class="btn btn-default" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
20 20 ${_('Download file')}
21 21 </a>
22 22 % endif
23 23
24 24 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
25 25 ## on branch head, can edit files
26 %if c.on_branch_head and c.branch_or_raw_id and not c.file.is_binary:
26 %if c.on_branch_head and c.branch_or_raw_id:
27 27 ## binary files are delete only
28 28 % if c.file.is_binary:
29 29 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))}
30 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit'),class_="btn btn-danger")}
30 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path),class_="btn btn-danger")}
31 31 % else:
32 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit')}">
32 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path)}">
33 33 ${_('Edit on branch: ')}<code>${c.branch_name}</code>
34 34 </a>
35 35
36 <a class="btn btn-danger" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _anchor='edit')}">
36 <a class="btn btn-danger" href="${h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path)}">
37 37 ${_('Delete')}
38 38 </a>
39 39 % endif
40 40 ## not on head, forbid all
41 41 % else:
42 42 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing files allowed only when on branch head commit'))}
43 43 ${h.link_to(_('Delete'), '#Delete', class_="btn btn-default btn-danger disabled tooltip", title=_('Deleting files allowed only when on branch head commit'))}
44 44 % endif
45 45 %endif
46 46
47 47 </div>
48 48 </div>
49 49 <div id="file_history_container"></div>
50 50
51 51 </div>
52 52 </div>
53 53
54 54 <div class="codeblock">
55 55 <div class=" codeblock-header">
56 56 <div class="file-filename">
57 57 <i class="icon-file"></i> ${c.file}
58 58 </div>
59 59
60 60 <div class="file-stats">
61 61
62 62 <div class="stats-info">
63 63 <span class="stats-first-item">${c.file.lines()[0]} ${_ungettext('line', 'lines', c.file.lines()[0])}</span>
64 64
65 65 <span> | ${h.format_byte_size_binary(c.file.size)}</span>
66 66 % if c.lf_node:
67 67 <span title="${_('This file is a pointer to large binary file')}"> | ${_('LargeFile')} ${h.format_byte_size_binary(c.lf_node.size)} </span>
68 68 % endif
69 69 <span> | ${c.file.mimetype} </span>
70 70 <span> | ${h.get_lexer_for_filenode(c.file).__class__.__name__}</span>
71 71 </div>
72 72 </div>
73 73 </div>
74 74
75 75 <div class="path clear-fix">
76 76 <div class="pull-left">
77 77 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.file.path, request.GET.get('at'))}
78 78 </div>
79 79
80 80 <div class="pull-right stats">
81 81 <a id="file_history_overview" href="#loadHistory">
82 82 ${_('History')}
83 83 </a>
84 84 |
85 85 %if c.annotate:
86 86 ${h.link_to(_('Source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
87 87 %else:
88 88 ${h.link_to(_('Annotation'), h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
89 89 %endif
90 90 | ${h.link_to(_('Raw'), h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
91 91
92 92 </div>
93 93 <div class="clear-fix"></div>
94 94 </div>
95 95
96 96 <div class="code-body clear-fix ">
97 97
98 98 %if c.file.is_binary:
99 99 <% rendered_binary = h.render_binary(c.repo_name, c.file)%>
100 100 % if rendered_binary:
101 101 ${rendered_binary}
102 102 % else:
103 103 <div>
104 104 ${_('Binary file (%s)') % c.file.mimetype}
105 105 </div>
106 106 % endif
107 107 %else:
108 108 % if c.file.size < c.visual.cut_off_limit_file:
109 109 %if c.renderer and not c.annotate:
110 110 ## pick relative url based on renderer
111 111 <%
112 112 relative_urls = {
113 113 'raw': h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
114 114 'standard': h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),
115 115 }
116 116 %>
117 117 ${h.render(c.file.content, renderer=c.renderer, relative_urls=relative_urls)}
118 118 %else:
119 119 <table class="cb codehilite">
120 120 %if c.annotate:
121 121 <% color_hasher = h.color_hasher() %>
122 122 %for annotation, lines in c.annotated_lines:
123 123 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
124 124 %endfor
125 125 %else:
126 126 %for line_num, tokens in enumerate(c.lines, 1):
127 127 ${sourceblock.render_line(line_num, tokens)}
128 128 %endfor
129 129 %endif
130 130 </table>
131 131 %endif
132 132 %else:
133 133 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
134 134 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
135 135 %endif
136 136 %endif
137 137 </div>
138 138
139 139 </div>
140 140
141 141 <script type="text/javascript">
142 142 % if request.GET.get('mark'):
143 143
144 144 $(function(){
145 145 $(".codehilite").mark(
146 146 "${request.GET.get('mark')}",
147 147 {
148 148 "className": 'match',
149 149 "accuracy": "complementary",
150 150 "ignorePunctuation": ":._(){}[]!'+=".split(""),
151 151 "each": function(el) {
152 152 // and also highlight lines !
153 153 $($(el).closest('tr')).find('td.cb-lineno').addClass('cb-line-selected');
154 154 }
155 155 }
156 156 );
157 157
158 158 });
159 159 % endif
160 160 </script>
@@ -1,190 +1,207 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 ${_('%s Files Add') % c.repo_name}
4 ${_('{} Files Upload').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 <%def name="breadcrumbs_links()">
15 ${_('Add new file')} @ ${h.show_id(c.commit)} ${_('Branch')}: ${c.commit.branch}
16 </%def>
14 <%def name="breadcrumbs_links()"></%def>
17 15
18 16 <%def name="menu_bar_subnav()">
19 17 ${self.repo_menu(active='files')}
20 18 </%def>
21 19
22 20 <%def name="main()">
21
23 22 <div class="box">
23 ## Template for uploads
24 <div style="display: none" id="tpl-dropzone">
25 <div class="dz-preview dz-file-preview">
26 <div class="dz-details">
27
28 <div class="dz-filename">
29 <span data-dz-name></span>
30 </div>
31 <div class="dz-filename-size">
32 <span class="dz-size" data-dz-size></span>
24 33
25 <div class="edit-file-title">
26 ${self.breadcrumbs()}
34 </div>
35
36 <div class="dz-sending" style="display: none">${_('Uploading...')}</div>
37 <div class="dz-response" style="display: none">
38 ${_('Uploaded')} 100%
39 </div>
40
41 </div>
42 <div class="dz-progress">
43 <span class="dz-upload" data-dz-uploadprogress></span>
44 </div>
45
46 <div class="dz-error-message">
47 </div>
48 </div>
27 49 </div>
28 50
29 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', enctype="multipart/form-data", class_="form-horizontal", request=request)}
51 <div class="edit-file-title">
52 <span class="title-heading">${_('Upload new file')} @ <code>${h.show_id(c.commit)}</code></span>
53 <span class="tag branchtag"><i class="icon-branch"></i> ${c.commit.branch}</span>
54 </div>
55
56 <% form_url = h.route_path('repo_files_upload_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
57 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
30 58 <div class="edit-file-fieldset">
31 <div class="fieldset">
32 <div id="destination-label" class="left-label">
33 ${_('Path')}:
34 </div>
35 <div class="right-content">
59 <div class="path-items">
60 <ul>
61 <li class="breadcrumb-path">
36 62 <div>
37 ${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path, request.GET.get('at'))} /
38 <input type="input-small" value="${c.f_path}" size="46" name="location" id="location">
63 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
64 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a> ${('/' if c.f_path else '')}
39 65 </div>
40 </div>
66 </li>
67 <li class="location-path">
68
69 </li>
70 </ul>
41 71 </div>
42 72
43 <div id="upload_file_container" class="fieldset">
44 <div class="filename-label left-label">
45 ${_('Filename')}:
46 </div>
47 <div class="right-content">
48 <input class="input-small" type="text" value="" size="46" name="filename_upload" id="filename_upload" placeholder="${_('No file selected')}">
49 </div>
50 <div class="filename-label left-label file-upload-label">
51 ${_('Upload file')}:
52 </div>
53 <div class="right-content file-upload-input">
54 <label for="upload_file" class="btn btn-default">Browse</label>
73 </div>
74
75 <div class="upload-form table">
76 <div id="files_data">
55 77
56 <input type="file" name="upload_file" id="upload_file">
78 <div class="dropzone-wrapper" id="file-uploader">
79 <div class="dropzone-pure">
80 <div class="dz-message">
81 <i class="icon-upload" style="font-size:36px"></i></br>
82 ${_("Drag'n Drop files here or")} <span class="link">${_('Choose your files')}</span>.<br>
83 </div>
84 </div>
85
57 86 </div>
58 87 </div>
59 88
60 89 </div>
61 90
62 <div class="table">
63 <div id="files_data">
64 <div id="codeblock" class="codeblock">
65 <div class="code-header form" id="set_mode_header">
66 <div class="fields">
67 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
68 <label for="line_wrap">${_('line wraps')}</label>
69 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
70
71 <div id="render_preview" class="btn btn-small preview hidden" >${_('Preview')}</div>
72 </div>
91 <div class="upload-form edit-file-fieldset">
92 <div class="fieldset">
93 <div class="message">
94 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
73 95 </div>
74 <div id="editor_container">
75 <pre id="editor_pre"></pre>
76 <textarea id="editor" name="content" ></textarea>
77 <div id="editor_preview"></div>
78 </div>
79 </div>
96 </div>
97 <div class="pull-left">
98 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
80 99 </div>
81 100 </div>
101 ##${h.end_form()}
82 102
83 <div class="edit-file-fieldset">
84 <div class="fieldset">
85 <div id="commit-message-label" class="commit-message-label left-label">
86 ${_('Commit Message')}:
87 </div>
88 <div class="right-content">
89 <div class="message">
90 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
91 </div>
92 </div>
93 </div>
94 <div class="pull-right">
95 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
96 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-success")}
97 </div>
103 <div class="file-upload-transaction-wrapper" style="display: none">
104 <div class="file-upload-transaction">
105 <h3>${_('Commiting...')}</h3>
106 <p>${_('Please wait while the files are being uploaded')}</p>
107 <p class="error" style="display: none">
108
109 </p>
110 <i class="icon-spin animate-spin"></i>
111 <p></p>
98 112 </div>
99 ${h.end_form()}
113 </div>
114
100 115 </div>
116
101 117 <script type="text/javascript">
102 118
103 $('#commit_btn').on('click', function() {
104 var button = $(this);
105 if (button.hasClass('clicked')) {
106 button.attr('disabled', true);
107 } else {
108 button.addClass('clicked');
109 }
110 });
111
112 var hide_upload = function(){
113 $('#files_data').show();
114 $('#upload_file_container').hide();
115 $('#filename_container').show();
116 };
119 $(document).ready(function () {
117 120
118 $('#file_enable').on('click', function(e){
119 e.preventDefault();
120 hide_upload();
121 });
122
123 var renderer = "";
124 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}";
125 var myCodeMirror = initCodeMirror('editor', reset_url, false);
126
127 var modes_select = $('#set_mode');
128 fillCodeMirrorOptions(modes_select);
121 //see: https://www.dropzonejs.com/#configuration
122 myDropzone = new Dropzone("div#file-uploader", {
123 url: "${form_url}",
124 headers: {"X-CSRF-Token": CSRF_TOKEN},
125 paramName: function () {
126 return "files_upload"
127 }, // The name that will be used to transfer the file
128 parallelUploads: 20,
129 maxFiles: 20,
130 uploadMultiple: true,
131 //chunking: true, // use chunking transfer, not supported at the moment
132 //maxFilesize: 2, // in MBs
133 autoProcessQueue: false, // if false queue will not be processed automatically.
134 createImageThumbnails: false,
135 previewTemplate: document.querySelector('#tpl-dropzone').innerHTML,
136 accept: function (file, done) {
137 done();
138 },
139 init: function () {
140 this.on("addedfile", function (file) {
129 141
130 var filename_selector = '#filename';
131 var callback = function(filename, mimetype, mode){
132 CodeMirrorPreviewEnable(mode);
133 };
134 // on change of select field set mode
135 setCodeMirrorModeFromSelect(
136 modes_select, filename_selector, myCodeMirror, callback);
142 });
143
144 this.on("sending", function (file, xhr, formData) {
145 formData.append("message", $('#commit').val());
146 $(file.previewElement).find('.dz-sending').show();
147 });
137 148
138 // on entering the new filename set mode, from given extension
139 setCodeMirrorModeFromInput(
140 modes_select, filename_selector, myCodeMirror, callback);
149 this.on("success", function (file, response) {
150 $(file.previewElement).find('.dz-sending').hide();
151 $(file.previewElement).find('.dz-response').show();
152
153 if (response.error !== null) {
154 $('.file-upload-transaction-wrapper .error').html('ERROR: {0}'.format(response.error));
155 $('.file-upload-transaction-wrapper .error').show();
156 $('.file-upload-transaction-wrapper i').hide()
157 }
158
159 var redirect_url = response.redirect_url || '/';
160 window.location = redirect_url
141 161
142 // if the file is renderable set line wraps automatically
143 if (renderer !== ""){
144 var line_wrap = 'on';
145 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
146 setCodeMirrorLineWrap(myCodeMirror, true);
147 }
162 });
163
164 this.on("error", function (file, errorMessage, xhr) {
165 var error = null;
148 166
149 // on select line wraps change the editor
150 $('#line_wrap').on('change', function(e){
151 var selected = e.currentTarget;
152 var line_wraps = {'on': true, 'off': false}[selected.value];
153 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
154 });
167 if (xhr !== undefined){
168 var httpStatus = xhr.status + " " + xhr.statusText;
169 if (xhr.status >= 500) {
170 error = httpStatus;
171 }
172 }
155 173
156 // render preview/edit button
157 $('#render_preview').on('click', function(e){
158 if($(this).hasClass('preview')){
159 $(this).removeClass('preview');
160 $(this).html("${_('Edit')}");
161 $('#editor_preview').show();
162 $(myCodeMirror.getWrapperElement()).hide();
174 if (error === null) {
175 error = errorMessage.error || errorMessage || httpStatus;
176 }
177
178 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
179 });
180 }
181 });
163 182
164 var possible_renderer = {
165 'rst':'rst',
166 'markdown':'markdown',
167 'gfm': 'markdown'}[myCodeMirror.getMode().name];
168 var _text = myCodeMirror.getValue();
169 var _renderer = possible_renderer || DEFAULT_RENDERER;
170 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
171 $('#editor_preview').html(_gettext('Loading ...'));
172 var url = pyroutes.url('repo_commit_comment_preview',
173 {'repo_name': '${c.repo_name}',
174 'commit_id': '${c.commit.raw_id}'});
183 $('#commit_btn').on('click', function(e) {
184 e.preventDefault();
185 var button = $(this);
186 if (button.hasClass('clicked')) {
187 button.attr('disabled', true);
188 } else {
189 button.addClass('clicked');
190 }
175 191
176 ajaxPOST(url, post_data, function(o){
177 $('#editor_preview').html(o);
178 })
179 }
180 else{
181 $(this).addClass('preview');
182 $(this).html("${_('Preview')}");
183 $('#editor_preview').hide();
184 $(myCodeMirror.getWrapperElement()).show();
185 }
192 var files = myDropzone.getQueuedFiles();
193 if (files.length === 0) {
194 alert("Missing files");
195 e.preventDefault();
196 }
197
198 $('.upload-form').hide();
199 $('.file-upload-transaction-wrapper').show();
200 myDropzone.processQueue();
201
202 });
203
186 204 });
187 $('#filename').focus();
188 205
189 206 </script>
190 207 </%def>
General Comments 0
You need to be logged in to leave comments. Login now