##// END OF EJS Templates
annotation: added shortcut links to browse the annotation view with previous commits.
marcink -
r1413:44a048ec default
parent child Browse files
Show More
@@ -1,1167 +1,1173 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('user_profile', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
203 203 action='user_profile')
204 204
205 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 206 rmap.connect('rst_help',
207 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 208 _static=True)
209 209 rmap.connect('markdown_help',
210 210 'http://daringfireball.net/projects/markdown/syntax',
211 211 _static=True)
212 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 215 # TODO: anderson - making this a static link since redirect won't play
216 216 # nice with POST requests
217 217 rmap.connect('enterprise_license_convert_from_old',
218 218 'https://rhodecode.com/u/license-upgrade',
219 219 _static=True)
220 220
221 221 routing_links.connect_redirection_links(rmap)
222 222
223 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225 225
226 226 # ADMIN REPOSITORY ROUTES
227 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 228 controller='admin/repos') as m:
229 229 m.connect('repos', '/repos',
230 230 action='create', conditions={'method': ['POST']})
231 231 m.connect('repos', '/repos',
232 232 action='index', conditions={'method': ['GET']})
233 233 m.connect('new_repo', '/create_repository', jsroute=True,
234 234 action='create_repository', conditions={'method': ['GET']})
235 235 m.connect('/repos/{repo_name}',
236 236 action='update', conditions={'method': ['PUT'],
237 237 'function': check_repo},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('delete_repo', '/repos/{repo_name}',
240 240 action='delete', conditions={'method': ['DELETE']},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242 m.connect('repo', '/repos/{repo_name}',
243 243 action='show', conditions={'method': ['GET'],
244 244 'function': check_repo},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246
247 247 # ADMIN REPOSITORY GROUPS ROUTES
248 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 249 controller='admin/repo_groups') as m:
250 250 m.connect('repo_groups', '/repo_groups',
251 251 action='create', conditions={'method': ['POST']})
252 252 m.connect('repo_groups', '/repo_groups',
253 253 action='index', conditions={'method': ['GET']})
254 254 m.connect('new_repo_group', '/repo_groups/new',
255 255 action='new', conditions={'method': ['GET']})
256 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 257 action='update', conditions={'method': ['PUT'],
258 258 'function': check_group},
259 259 requirements=URL_NAME_REQUIREMENTS)
260 260
261 261 # EXTRAS REPO GROUP ROUTES
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['GET'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 267 action='edit',
268 268 conditions={'method': ['PUT'], 'function': check_group},
269 269 requirements=URL_NAME_REQUIREMENTS)
270 270
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['GET'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 276 action='edit_repo_group_advanced',
277 277 conditions={'method': ['PUT'], 'function': check_group},
278 278 requirements=URL_NAME_REQUIREMENTS)
279 279
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='edit_repo_group_perms',
282 282 conditions={'method': ['GET'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 285 action='update_perms',
286 286 conditions={'method': ['PUT'], 'function': check_group},
287 287 requirements=URL_NAME_REQUIREMENTS)
288 288
289 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 290 action='delete', conditions={'method': ['DELETE'],
291 291 'function': check_group},
292 292 requirements=URL_NAME_REQUIREMENTS)
293 293
294 294 # ADMIN USER ROUTES
295 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 296 controller='admin/users') as m:
297 297 m.connect('users', '/users',
298 298 action='create', conditions={'method': ['POST']})
299 299 m.connect('users', '/users',
300 300 action='index', conditions={'method': ['GET']})
301 301 m.connect('new_user', '/users/new',
302 302 action='new', conditions={'method': ['GET']})
303 303 m.connect('update_user', '/users/{user_id}',
304 304 action='update', conditions={'method': ['PUT']})
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 308 action='edit', conditions={'method': ['GET']}, jsroute=True)
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 312 action='reset_password', conditions={'method': ['POST']})
313 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 314 action='create_personal_repo_group', conditions={'method': ['POST']})
315 315
316 316 # EXTRAS USER ROUTES
317 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 318 action='edit_advanced', conditions={'method': ['GET']})
319 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 320 action='update_advanced', conditions={'method': ['PUT']})
321 321
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 325 action='add_auth_token', conditions={'method': ['PUT']})
326 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 327 action='delete_auth_token', conditions={'method': ['DELETE']})
328 328
329 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 330 action='edit_global_perms', conditions={'method': ['GET']})
331 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 332 action='update_global_perms', conditions={'method': ['PUT']})
333 333
334 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 335 action='edit_perms_summary', conditions={'method': ['GET']})
336 336
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='edit_emails', conditions={'method': ['GET']})
339 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 340 action='add_email', conditions={'method': ['PUT']})
341 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 342 action='delete_email', conditions={'method': ['DELETE']})
343 343
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='edit_ips', conditions={'method': ['GET']})
346 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 347 action='add_ip', conditions={'method': ['PUT']})
348 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 349 action='delete_ip', conditions={'method': ['DELETE']})
350 350
351 351 # ADMIN USER GROUPS REST ROUTES
352 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 353 controller='admin/user_groups') as m:
354 354 m.connect('users_groups', '/user_groups',
355 355 action='create', conditions={'method': ['POST']})
356 356 m.connect('users_groups', '/user_groups',
357 357 action='index', conditions={'method': ['GET']})
358 358 m.connect('new_users_group', '/user_groups/new',
359 359 action='new', conditions={'method': ['GET']})
360 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 361 action='update', conditions={'method': ['PUT']})
362 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 363 action='delete', conditions={'method': ['DELETE']})
364 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 365 action='edit', conditions={'method': ['GET']},
366 366 function=check_user_group)
367 367
368 368 # EXTRAS USER GROUP ROUTES
369 369 m.connect('edit_user_group_global_perms',
370 370 '/user_groups/{user_group_id}/edit/global_permissions',
371 371 action='edit_global_perms', conditions={'method': ['GET']})
372 372 m.connect('edit_user_group_global_perms',
373 373 '/user_groups/{user_group_id}/edit/global_permissions',
374 374 action='update_global_perms', conditions={'method': ['PUT']})
375 375 m.connect('edit_user_group_perms_summary',
376 376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 377 action='edit_perms_summary', conditions={'method': ['GET']})
378 378
379 379 m.connect('edit_user_group_perms',
380 380 '/user_groups/{user_group_id}/edit/permissions',
381 381 action='edit_perms', conditions={'method': ['GET']})
382 382 m.connect('edit_user_group_perms',
383 383 '/user_groups/{user_group_id}/edit/permissions',
384 384 action='update_perms', conditions={'method': ['PUT']})
385 385
386 386 m.connect('edit_user_group_advanced',
387 387 '/user_groups/{user_group_id}/edit/advanced',
388 388 action='edit_advanced', conditions={'method': ['GET']})
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 392 action='user_group_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 396 controller='admin/permissions') as m:
397 397 m.connect('admin_permissions_application', '/permissions/application',
398 398 action='permission_application_update', conditions={'method': ['POST']})
399 399 m.connect('admin_permissions_application', '/permissions/application',
400 400 action='permission_application', conditions={'method': ['GET']})
401 401
402 402 m.connect('admin_permissions_global', '/permissions/global',
403 403 action='permission_global_update', conditions={'method': ['POST']})
404 404 m.connect('admin_permissions_global', '/permissions/global',
405 405 action='permission_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_permissions_object', '/permissions/object',
408 408 action='permission_objects_update', conditions={'method': ['POST']})
409 409 m.connect('admin_permissions_object', '/permissions/object',
410 410 action='permission_objects', conditions={'method': ['GET']})
411 411
412 412 m.connect('admin_permissions_ips', '/permissions/ips',
413 413 action='permission_ips', conditions={'method': ['POST']})
414 414 m.connect('admin_permissions_ips', '/permissions/ips',
415 415 action='permission_ips', conditions={'method': ['GET']})
416 416
417 417 m.connect('admin_permissions_overview', '/permissions/overview',
418 418 action='permission_perms', conditions={'method': ['GET']})
419 419
420 420 # ADMIN DEFAULTS REST ROUTES
421 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 422 controller='admin/defaults') as m:
423 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 424 action='update_repository_defaults', conditions={'method': ['POST']})
425 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 426 action='index', conditions={'method': ['GET']})
427 427
428 428 # ADMIN DEBUG STYLE ROUTES
429 429 if str2bool(config.get('debug_style')):
430 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 431 controller='debug_style') as m:
432 432 m.connect('debug_style_home', '',
433 433 action='index', conditions={'method': ['GET']})
434 434 m.connect('debug_style_template', '/t/{t_path}',
435 435 action='template', conditions={'method': ['GET']})
436 436
437 437 # ADMIN SETTINGS ROUTES
438 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 439 controller='admin/settings') as m:
440 440
441 441 # default
442 442 m.connect('admin_settings', '/settings',
443 443 action='settings_global_update',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_settings', '/settings',
446 446 action='settings_global', conditions={'method': ['GET']})
447 447
448 448 m.connect('admin_settings_vcs', '/settings/vcs',
449 449 action='settings_vcs_update',
450 450 conditions={'method': ['POST']})
451 451 m.connect('admin_settings_vcs', '/settings/vcs',
452 452 action='settings_vcs',
453 453 conditions={'method': ['GET']})
454 454 m.connect('admin_settings_vcs', '/settings/vcs',
455 455 action='delete_svn_pattern',
456 456 conditions={'method': ['DELETE']})
457 457
458 458 m.connect('admin_settings_mapping', '/settings/mapping',
459 459 action='settings_mapping_update',
460 460 conditions={'method': ['POST']})
461 461 m.connect('admin_settings_mapping', '/settings/mapping',
462 462 action='settings_mapping', conditions={'method': ['GET']})
463 463
464 464 m.connect('admin_settings_global', '/settings/global',
465 465 action='settings_global_update',
466 466 conditions={'method': ['POST']})
467 467 m.connect('admin_settings_global', '/settings/global',
468 468 action='settings_global', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_visual', '/settings/visual',
471 471 action='settings_visual_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_visual', '/settings/visual',
474 474 action='settings_visual', conditions={'method': ['GET']})
475 475
476 476 m.connect('admin_settings_issuetracker',
477 477 '/settings/issue-tracker', action='settings_issuetracker',
478 478 conditions={'method': ['GET']})
479 479 m.connect('admin_settings_issuetracker_save',
480 480 '/settings/issue-tracker/save',
481 481 action='settings_issuetracker_save',
482 482 conditions={'method': ['POST']})
483 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 484 action='settings_issuetracker_test',
485 485 conditions={'method': ['POST']})
486 486 m.connect('admin_issuetracker_delete',
487 487 '/settings/issue-tracker/delete',
488 488 action='settings_issuetracker_delete',
489 489 conditions={'method': ['DELETE']})
490 490
491 491 m.connect('admin_settings_email', '/settings/email',
492 492 action='settings_email_update',
493 493 conditions={'method': ['POST']})
494 494 m.connect('admin_settings_email', '/settings/email',
495 495 action='settings_email', conditions={'method': ['GET']})
496 496
497 497 m.connect('admin_settings_hooks', '/settings/hooks',
498 498 action='settings_hooks_update',
499 499 conditions={'method': ['POST', 'DELETE']})
500 500 m.connect('admin_settings_hooks', '/settings/hooks',
501 501 action='settings_hooks', conditions={'method': ['GET']})
502 502
503 503 m.connect('admin_settings_search', '/settings/search',
504 504 action='settings_search', conditions={'method': ['GET']})
505 505
506 506 m.connect('admin_settings_supervisor', '/settings/supervisor',
507 507 action='settings_supervisor', conditions={'method': ['GET']})
508 508 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
509 509 action='settings_supervisor_log', conditions={'method': ['GET']})
510 510
511 511 m.connect('admin_settings_labs', '/settings/labs',
512 512 action='settings_labs_update',
513 513 conditions={'method': ['POST']})
514 514 m.connect('admin_settings_labs', '/settings/labs',
515 515 action='settings_labs', conditions={'method': ['GET']})
516 516
517 517 # ADMIN MY ACCOUNT
518 518 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 519 controller='admin/my_account') as m:
520 520
521 521 m.connect('my_account', '/my_account',
522 522 action='my_account', conditions={'method': ['GET']})
523 523 m.connect('my_account_edit', '/my_account/edit',
524 524 action='my_account_edit', conditions={'method': ['GET']})
525 525 m.connect('my_account', '/my_account',
526 526 action='my_account_update', conditions={'method': ['POST']})
527 527
528 528 m.connect('my_account_password', '/my_account/password',
529 529 action='my_account_password', conditions={'method': ['GET', 'POST']})
530 530
531 531 m.connect('my_account_repos', '/my_account/repos',
532 532 action='my_account_repos', conditions={'method': ['GET']})
533 533
534 534 m.connect('my_account_watched', '/my_account/watched',
535 535 action='my_account_watched', conditions={'method': ['GET']})
536 536
537 537 m.connect('my_account_pullrequests', '/my_account/pull_requests',
538 538 action='my_account_pullrequests', conditions={'method': ['GET']})
539 539
540 540 m.connect('my_account_perms', '/my_account/perms',
541 541 action='my_account_perms', conditions={'method': ['GET']})
542 542
543 543 m.connect('my_account_emails', '/my_account/emails',
544 544 action='my_account_emails', conditions={'method': ['GET']})
545 545 m.connect('my_account_emails', '/my_account/emails',
546 546 action='my_account_emails_add', conditions={'method': ['POST']})
547 547 m.connect('my_account_emails', '/my_account/emails',
548 548 action='my_account_emails_delete', conditions={'method': ['DELETE']})
549 549
550 550 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
551 551 action='my_account_auth_tokens', conditions={'method': ['GET']})
552 552 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
553 553 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
554 554 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
555 555 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
556 556 m.connect('my_account_notifications', '/my_account/notifications',
557 557 action='my_notifications',
558 558 conditions={'method': ['GET']})
559 559 m.connect('my_account_notifications_toggle_visibility',
560 560 '/my_account/toggle_visibility',
561 561 action='my_notifications_toggle_visibility',
562 562 conditions={'method': ['POST']})
563 563 m.connect('my_account_notifications_test_channelstream',
564 564 '/my_account/test_channelstream',
565 565 action='my_account_notifications_test_channelstream',
566 566 conditions={'method': ['POST']})
567 567
568 568 # NOTIFICATION REST ROUTES
569 569 with rmap.submapper(path_prefix=ADMIN_PREFIX,
570 570 controller='admin/notifications') as m:
571 571 m.connect('notifications', '/notifications',
572 572 action='index', conditions={'method': ['GET']})
573 573 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
574 574 action='mark_all_read', conditions={'method': ['POST']})
575 575 m.connect('/notifications/{notification_id}',
576 576 action='update', conditions={'method': ['PUT']})
577 577 m.connect('/notifications/{notification_id}',
578 578 action='delete', conditions={'method': ['DELETE']})
579 579 m.connect('notification', '/notifications/{notification_id}',
580 580 action='show', conditions={'method': ['GET']})
581 581
582 582 # ADMIN GIST
583 583 with rmap.submapper(path_prefix=ADMIN_PREFIX,
584 584 controller='admin/gists') as m:
585 585 m.connect('gists', '/gists',
586 586 action='create', conditions={'method': ['POST']})
587 587 m.connect('gists', '/gists', jsroute=True,
588 588 action='index', conditions={'method': ['GET']})
589 589 m.connect('new_gist', '/gists/new', jsroute=True,
590 590 action='new', conditions={'method': ['GET']})
591 591
592 592 m.connect('/gists/{gist_id}',
593 593 action='delete', conditions={'method': ['DELETE']})
594 594 m.connect('edit_gist', '/gists/{gist_id}/edit',
595 595 action='edit_form', conditions={'method': ['GET']})
596 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
597 597 action='edit', conditions={'method': ['POST']})
598 598 m.connect(
599 599 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
600 600 action='check_revision', conditions={'method': ['GET']})
601 601
602 602 m.connect('gist', '/gists/{gist_id}',
603 603 action='show', conditions={'method': ['GET']})
604 604 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
605 605 revision='tip',
606 606 action='show', conditions={'method': ['GET']})
607 607 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
608 608 revision='tip',
609 609 action='show', conditions={'method': ['GET']})
610 610 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
611 611 revision='tip',
612 612 action='show', conditions={'method': ['GET']},
613 613 requirements=URL_NAME_REQUIREMENTS)
614 614
615 615 # ADMIN MAIN PAGES
616 616 with rmap.submapper(path_prefix=ADMIN_PREFIX,
617 617 controller='admin/admin') as m:
618 618 m.connect('admin_home', '', action='index')
619 619 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
620 620 action='add_repo')
621 621 m.connect(
622 622 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
623 623 action='pull_requests')
624 624 m.connect(
625 625 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
626 626 action='pull_requests')
627 627 m.connect(
628 628 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
629 629 action='pull_requests')
630 630
631 631 # USER JOURNAL
632 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
633 633 controller='journal', action='index')
634 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
635 635 controller='journal', action='journal_rss')
636 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
637 637 controller='journal', action='journal_atom')
638 638
639 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
640 640 controller='journal', action='public_journal')
641 641
642 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
643 643 controller='journal', action='public_journal_rss')
644 644
645 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
646 646 controller='journal', action='public_journal_rss')
647 647
648 648 rmap.connect('public_journal_atom',
649 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
650 650 action='public_journal_atom')
651 651
652 652 rmap.connect('public_journal_atom_old',
653 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
654 654 action='public_journal_atom')
655 655
656 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
657 657 controller='journal', action='toggle_following', jsroute=True,
658 658 conditions={'method': ['POST']})
659 659
660 660 # FULL TEXT SEARCH
661 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
662 662 controller='search')
663 663 rmap.connect('search_repo_home', '/{repo_name}/search',
664 664 controller='search',
665 665 action='index',
666 666 conditions={'function': check_repo},
667 667 requirements=URL_NAME_REQUIREMENTS)
668 668
669 669 # FEEDS
670 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
671 671 controller='feed', action='rss',
672 672 conditions={'function': check_repo},
673 673 requirements=URL_NAME_REQUIREMENTS)
674 674
675 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
676 676 controller='feed', action='atom',
677 677 conditions={'function': check_repo},
678 678 requirements=URL_NAME_REQUIREMENTS)
679 679
680 680 #==========================================================================
681 681 # REPOSITORY ROUTES
682 682 #==========================================================================
683 683
684 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
685 685 controller='admin/repos', action='repo_creating',
686 686 requirements=URL_NAME_REQUIREMENTS)
687 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
688 688 controller='admin/repos', action='repo_check',
689 689 requirements=URL_NAME_REQUIREMENTS)
690 690
691 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
692 692 controller='summary', action='repo_stats',
693 693 conditions={'function': check_repo},
694 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695 695
696 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
697 697 controller='summary', action='repo_refs_data',
698 698 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
699 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 700 controller='summary', action='repo_refs_changelog_data',
701 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 703 controller='summary', action='repo_default_reviewers_data',
704 704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
705 705
706 706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
707 707 controller='changeset', revision='tip',
708 708 conditions={'function': check_repo},
709 709 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
710 710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
711 711 controller='changeset', revision='tip', action='changeset_children',
712 712 conditions={'function': check_repo},
713 713 requirements=URL_NAME_REQUIREMENTS)
714 714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
715 715 controller='changeset', revision='tip', action='changeset_parents',
716 716 conditions={'function': check_repo},
717 717 requirements=URL_NAME_REQUIREMENTS)
718 718
719 719 # repo edit options
720 720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
721 721 controller='admin/repos', action='edit',
722 722 conditions={'method': ['GET'], 'function': check_repo},
723 723 requirements=URL_NAME_REQUIREMENTS)
724 724
725 725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
726 726 jsroute=True,
727 727 controller='admin/repos', action='edit_permissions',
728 728 conditions={'method': ['GET'], 'function': check_repo},
729 729 requirements=URL_NAME_REQUIREMENTS)
730 730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
731 731 controller='admin/repos', action='edit_permissions_update',
732 732 conditions={'method': ['PUT'], 'function': check_repo},
733 733 requirements=URL_NAME_REQUIREMENTS)
734 734
735 735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
736 736 controller='admin/repos', action='edit_fields',
737 737 conditions={'method': ['GET'], 'function': check_repo},
738 738 requirements=URL_NAME_REQUIREMENTS)
739 739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
740 740 controller='admin/repos', action='create_repo_field',
741 741 conditions={'method': ['PUT'], 'function': check_repo},
742 742 requirements=URL_NAME_REQUIREMENTS)
743 743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
744 744 controller='admin/repos', action='delete_repo_field',
745 745 conditions={'method': ['DELETE'], 'function': check_repo},
746 746 requirements=URL_NAME_REQUIREMENTS)
747 747
748 748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
749 749 controller='admin/repos', action='edit_advanced',
750 750 conditions={'method': ['GET'], 'function': check_repo},
751 751 requirements=URL_NAME_REQUIREMENTS)
752 752
753 753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
754 754 controller='admin/repos', action='edit_advanced_locking',
755 755 conditions={'method': ['PUT'], 'function': check_repo},
756 756 requirements=URL_NAME_REQUIREMENTS)
757 757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
758 758 controller='admin/repos', action='toggle_locking',
759 759 conditions={'method': ['GET'], 'function': check_repo},
760 760 requirements=URL_NAME_REQUIREMENTS)
761 761
762 762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
763 763 controller='admin/repos', action='edit_advanced_journal',
764 764 conditions={'method': ['PUT'], 'function': check_repo},
765 765 requirements=URL_NAME_REQUIREMENTS)
766 766
767 767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
768 768 controller='admin/repos', action='edit_advanced_fork',
769 769 conditions={'method': ['PUT'], 'function': check_repo},
770 770 requirements=URL_NAME_REQUIREMENTS)
771 771
772 772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 773 controller='admin/repos', action='edit_caches_form',
774 774 conditions={'method': ['GET'], 'function': check_repo},
775 775 requirements=URL_NAME_REQUIREMENTS)
776 776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
777 777 controller='admin/repos', action='edit_caches',
778 778 conditions={'method': ['PUT'], 'function': check_repo},
779 779 requirements=URL_NAME_REQUIREMENTS)
780 780
781 781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 782 controller='admin/repos', action='edit_remote_form',
783 783 conditions={'method': ['GET'], 'function': check_repo},
784 784 requirements=URL_NAME_REQUIREMENTS)
785 785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
786 786 controller='admin/repos', action='edit_remote',
787 787 conditions={'method': ['PUT'], 'function': check_repo},
788 788 requirements=URL_NAME_REQUIREMENTS)
789 789
790 790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 791 controller='admin/repos', action='edit_statistics_form',
792 792 conditions={'method': ['GET'], 'function': check_repo},
793 793 requirements=URL_NAME_REQUIREMENTS)
794 794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
795 795 controller='admin/repos', action='edit_statistics',
796 796 conditions={'method': ['PUT'], 'function': check_repo},
797 797 requirements=URL_NAME_REQUIREMENTS)
798 798 rmap.connect('repo_settings_issuetracker',
799 799 '/{repo_name}/settings/issue-tracker',
800 800 controller='admin/repos', action='repo_issuetracker',
801 801 conditions={'method': ['GET'], 'function': check_repo},
802 802 requirements=URL_NAME_REQUIREMENTS)
803 803 rmap.connect('repo_issuetracker_test',
804 804 '/{repo_name}/settings/issue-tracker/test',
805 805 controller='admin/repos', action='repo_issuetracker_test',
806 806 conditions={'method': ['POST'], 'function': check_repo},
807 807 requirements=URL_NAME_REQUIREMENTS)
808 808 rmap.connect('repo_issuetracker_delete',
809 809 '/{repo_name}/settings/issue-tracker/delete',
810 810 controller='admin/repos', action='repo_issuetracker_delete',
811 811 conditions={'method': ['DELETE'], 'function': check_repo},
812 812 requirements=URL_NAME_REQUIREMENTS)
813 813 rmap.connect('repo_issuetracker_save',
814 814 '/{repo_name}/settings/issue-tracker/save',
815 815 controller='admin/repos', action='repo_issuetracker_save',
816 816 conditions={'method': ['POST'], 'function': check_repo},
817 817 requirements=URL_NAME_REQUIREMENTS)
818 818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 819 controller='admin/repos', action='repo_settings_vcs_update',
820 820 conditions={'method': ['POST'], 'function': check_repo},
821 821 requirements=URL_NAME_REQUIREMENTS)
822 822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 823 controller='admin/repos', action='repo_settings_vcs',
824 824 conditions={'method': ['GET'], 'function': check_repo},
825 825 requirements=URL_NAME_REQUIREMENTS)
826 826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
827 827 controller='admin/repos', action='repo_delete_svn_pattern',
828 828 conditions={'method': ['DELETE'], 'function': check_repo},
829 829 requirements=URL_NAME_REQUIREMENTS)
830 830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 831 controller='admin/repos', action='repo_settings_pullrequest',
832 832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 833 requirements=URL_NAME_REQUIREMENTS)
834 834
835 835 # still working url for backward compat.
836 836 rmap.connect('raw_changeset_home_depraced',
837 837 '/{repo_name}/raw-changeset/{revision}',
838 838 controller='changeset', action='changeset_raw',
839 839 revision='tip', conditions={'function': check_repo},
840 840 requirements=URL_NAME_REQUIREMENTS)
841 841
842 842 # new URLs
843 843 rmap.connect('changeset_raw_home',
844 844 '/{repo_name}/changeset-diff/{revision}',
845 845 controller='changeset', action='changeset_raw',
846 846 revision='tip', conditions={'function': check_repo},
847 847 requirements=URL_NAME_REQUIREMENTS)
848 848
849 849 rmap.connect('changeset_patch_home',
850 850 '/{repo_name}/changeset-patch/{revision}',
851 851 controller='changeset', action='changeset_patch',
852 852 revision='tip', conditions={'function': check_repo},
853 853 requirements=URL_NAME_REQUIREMENTS)
854 854
855 855 rmap.connect('changeset_download_home',
856 856 '/{repo_name}/changeset-download/{revision}',
857 857 controller='changeset', action='changeset_download',
858 858 revision='tip', conditions={'function': check_repo},
859 859 requirements=URL_NAME_REQUIREMENTS)
860 860
861 861 rmap.connect('changeset_comment',
862 862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
863 863 controller='changeset', revision='tip', action='comment',
864 864 conditions={'function': check_repo},
865 865 requirements=URL_NAME_REQUIREMENTS)
866 866
867 867 rmap.connect('changeset_comment_preview',
868 868 '/{repo_name}/changeset/comment/preview', jsroute=True,
869 869 controller='changeset', action='preview_comment',
870 870 conditions={'function': check_repo, 'method': ['POST']},
871 871 requirements=URL_NAME_REQUIREMENTS)
872 872
873 873 rmap.connect('changeset_comment_delete',
874 874 '/{repo_name}/changeset/comment/{comment_id}/delete',
875 875 controller='changeset', action='delete_comment',
876 876 conditions={'function': check_repo, 'method': ['DELETE']},
877 877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
878 878
879 879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
880 880 controller='changeset', action='changeset_info',
881 881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
882 882
883 883 rmap.connect('compare_home',
884 884 '/{repo_name}/compare',
885 885 controller='compare', action='index',
886 886 conditions={'function': check_repo},
887 887 requirements=URL_NAME_REQUIREMENTS)
888 888
889 889 rmap.connect('compare_url',
890 890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
891 891 controller='compare', action='compare',
892 892 conditions={'function': check_repo},
893 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
894 894
895 895 rmap.connect('pullrequest_home',
896 896 '/{repo_name}/pull-request/new', controller='pullrequests',
897 897 action='index', conditions={'function': check_repo,
898 898 'method': ['GET']},
899 899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
900 900
901 901 rmap.connect('pullrequest',
902 902 '/{repo_name}/pull-request/new', controller='pullrequests',
903 903 action='create', conditions={'function': check_repo,
904 904 'method': ['POST']},
905 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906 906
907 907 rmap.connect('pullrequest_repo_refs',
908 908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
909 909 controller='pullrequests',
910 910 action='get_repo_refs',
911 911 conditions={'function': check_repo, 'method': ['GET']},
912 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913 913
914 914 rmap.connect('pullrequest_repo_destinations',
915 915 '/{repo_name}/pull-request/repo-destinations',
916 916 controller='pullrequests',
917 917 action='get_repo_destinations',
918 918 conditions={'function': check_repo, 'method': ['GET']},
919 919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
920 920
921 921 rmap.connect('pullrequest_show',
922 922 '/{repo_name}/pull-request/{pull_request_id}',
923 923 controller='pullrequests',
924 924 action='show', conditions={'function': check_repo,
925 925 'method': ['GET']},
926 926 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927 927
928 928 rmap.connect('pullrequest_update',
929 929 '/{repo_name}/pull-request/{pull_request_id}',
930 930 controller='pullrequests',
931 931 action='update', conditions={'function': check_repo,
932 932 'method': ['PUT']},
933 933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
934 934
935 935 rmap.connect('pullrequest_merge',
936 936 '/{repo_name}/pull-request/{pull_request_id}',
937 937 controller='pullrequests',
938 938 action='merge', conditions={'function': check_repo,
939 939 'method': ['POST']},
940 940 requirements=URL_NAME_REQUIREMENTS)
941 941
942 942 rmap.connect('pullrequest_delete',
943 943 '/{repo_name}/pull-request/{pull_request_id}',
944 944 controller='pullrequests',
945 945 action='delete', conditions={'function': check_repo,
946 946 'method': ['DELETE']},
947 947 requirements=URL_NAME_REQUIREMENTS)
948 948
949 949 rmap.connect('pullrequest_show_all',
950 950 '/{repo_name}/pull-request',
951 951 controller='pullrequests',
952 952 action='show_all', conditions={'function': check_repo,
953 953 'method': ['GET']},
954 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
955 955
956 956 rmap.connect('pullrequest_comment',
957 957 '/{repo_name}/pull-request-comment/{pull_request_id}',
958 958 controller='pullrequests',
959 959 action='comment', conditions={'function': check_repo,
960 960 'method': ['POST']},
961 961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
962 962
963 963 rmap.connect('pullrequest_comment_delete',
964 964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
965 965 controller='pullrequests', action='delete_comment',
966 966 conditions={'function': check_repo, 'method': ['DELETE']},
967 967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
968 968
969 969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
970 970 controller='summary', conditions={'function': check_repo},
971 971 requirements=URL_NAME_REQUIREMENTS)
972 972
973 973 rmap.connect('branches_home', '/{repo_name}/branches',
974 974 controller='branches', conditions={'function': check_repo},
975 975 requirements=URL_NAME_REQUIREMENTS)
976 976
977 977 rmap.connect('tags_home', '/{repo_name}/tags',
978 978 controller='tags', conditions={'function': check_repo},
979 979 requirements=URL_NAME_REQUIREMENTS)
980 980
981 981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
982 982 controller='bookmarks', conditions={'function': check_repo},
983 983 requirements=URL_NAME_REQUIREMENTS)
984 984
985 985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
986 986 controller='changelog', conditions={'function': check_repo},
987 987 requirements=URL_NAME_REQUIREMENTS)
988 988
989 989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
990 990 controller='changelog', action='changelog_summary',
991 991 conditions={'function': check_repo},
992 992 requirements=URL_NAME_REQUIREMENTS)
993 993
994 994 rmap.connect('changelog_file_home',
995 995 '/{repo_name}/changelog/{revision}/{f_path}',
996 996 controller='changelog', f_path=None,
997 997 conditions={'function': check_repo},
998 998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
999 999
1000 1000 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
1001 1001 controller='changelog', action='changelog_elements',
1002 1002 conditions={'function': check_repo},
1003 1003 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1004 1004
1005 1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
1006 1006 controller='files', revision='tip', f_path='',
1007 1007 conditions={'function': check_repo},
1008 1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1009 1009
1010 1010 rmap.connect('files_home_simple_catchrev',
1011 1011 '/{repo_name}/files/{revision}',
1012 1012 controller='files', revision='tip', f_path='',
1013 1013 conditions={'function': check_repo},
1014 1014 requirements=URL_NAME_REQUIREMENTS)
1015 1015
1016 1016 rmap.connect('files_home_simple_catchall',
1017 1017 '/{repo_name}/files',
1018 1018 controller='files', revision='tip', f_path='',
1019 1019 conditions={'function': check_repo},
1020 1020 requirements=URL_NAME_REQUIREMENTS)
1021 1021
1022 1022 rmap.connect('files_history_home',
1023 1023 '/{repo_name}/history/{revision}/{f_path}',
1024 1024 controller='files', action='history', revision='tip', f_path='',
1025 1025 conditions={'function': check_repo},
1026 1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1027 1027
1028 1028 rmap.connect('files_authors_home',
1029 1029 '/{repo_name}/authors/{revision}/{f_path}',
1030 1030 controller='files', action='authors', revision='tip', f_path='',
1031 1031 conditions={'function': check_repo},
1032 1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1033 1033
1034 1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1035 1035 controller='files', action='diff', f_path='',
1036 1036 conditions={'function': check_repo},
1037 1037 requirements=URL_NAME_REQUIREMENTS)
1038 1038
1039 1039 rmap.connect('files_diff_2way_home',
1040 1040 '/{repo_name}/diff-2way/{f_path}',
1041 1041 controller='files', action='diff_2way', f_path='',
1042 1042 conditions={'function': check_repo},
1043 1043 requirements=URL_NAME_REQUIREMENTS)
1044 1044
1045 1045 rmap.connect('files_rawfile_home',
1046 1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1047 1047 controller='files', action='rawfile', revision='tip',
1048 1048 f_path='', conditions={'function': check_repo},
1049 1049 requirements=URL_NAME_REQUIREMENTS)
1050 1050
1051 1051 rmap.connect('files_raw_home',
1052 1052 '/{repo_name}/raw/{revision}/{f_path}',
1053 1053 controller='files', action='raw', revision='tip', f_path='',
1054 1054 conditions={'function': check_repo},
1055 1055 requirements=URL_NAME_REQUIREMENTS)
1056 1056
1057 1057 rmap.connect('files_render_home',
1058 1058 '/{repo_name}/render/{revision}/{f_path}',
1059 1059 controller='files', action='index', revision='tip', f_path='',
1060 1060 rendered=True, conditions={'function': check_repo},
1061 1061 requirements=URL_NAME_REQUIREMENTS)
1062 1062
1063 1063 rmap.connect('files_annotate_home',
1064 1064 '/{repo_name}/annotate/{revision}/{f_path}',
1065 1065 controller='files', action='index', revision='tip',
1066 1066 f_path='', annotate=True, conditions={'function': check_repo},
1067 1067 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1068 1068
1069 rmap.connect('files_annotate_previous',
1070 '/{repo_name}/annotate-previous/{revision}/{f_path}',
1071 controller='files', action='annotate_previous', revision='tip',
1072 f_path='', annotate=True, conditions={'function': check_repo},
1073 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1074
1069 1075 rmap.connect('files_edit',
1070 1076 '/{repo_name}/edit/{revision}/{f_path}',
1071 1077 controller='files', action='edit', revision='tip',
1072 1078 f_path='',
1073 1079 conditions={'function': check_repo, 'method': ['POST']},
1074 1080 requirements=URL_NAME_REQUIREMENTS)
1075 1081
1076 1082 rmap.connect('files_edit_home',
1077 1083 '/{repo_name}/edit/{revision}/{f_path}',
1078 1084 controller='files', action='edit_home', revision='tip',
1079 1085 f_path='', conditions={'function': check_repo},
1080 1086 requirements=URL_NAME_REQUIREMENTS)
1081 1087
1082 1088 rmap.connect('files_add',
1083 1089 '/{repo_name}/add/{revision}/{f_path}',
1084 1090 controller='files', action='add', revision='tip',
1085 1091 f_path='',
1086 1092 conditions={'function': check_repo, 'method': ['POST']},
1087 1093 requirements=URL_NAME_REQUIREMENTS)
1088 1094
1089 1095 rmap.connect('files_add_home',
1090 1096 '/{repo_name}/add/{revision}/{f_path}',
1091 1097 controller='files', action='add_home', revision='tip',
1092 1098 f_path='', conditions={'function': check_repo},
1093 1099 requirements=URL_NAME_REQUIREMENTS)
1094 1100
1095 1101 rmap.connect('files_delete',
1096 1102 '/{repo_name}/delete/{revision}/{f_path}',
1097 1103 controller='files', action='delete', revision='tip',
1098 1104 f_path='',
1099 1105 conditions={'function': check_repo, 'method': ['POST']},
1100 1106 requirements=URL_NAME_REQUIREMENTS)
1101 1107
1102 1108 rmap.connect('files_delete_home',
1103 1109 '/{repo_name}/delete/{revision}/{f_path}',
1104 1110 controller='files', action='delete_home', revision='tip',
1105 1111 f_path='', conditions={'function': check_repo},
1106 1112 requirements=URL_NAME_REQUIREMENTS)
1107 1113
1108 1114 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1109 1115 controller='files', action='archivefile',
1110 1116 conditions={'function': check_repo},
1111 1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1112 1118
1113 1119 rmap.connect('files_nodelist_home',
1114 1120 '/{repo_name}/nodelist/{revision}/{f_path}',
1115 1121 controller='files', action='nodelist',
1116 1122 conditions={'function': check_repo},
1117 1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1118 1124
1119 1125 rmap.connect('files_nodetree_full',
1120 1126 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1121 1127 controller='files', action='nodetree_full',
1122 1128 conditions={'function': check_repo},
1123 1129 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1124 1130
1125 1131 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1126 1132 controller='forks', action='fork_create',
1127 1133 conditions={'function': check_repo, 'method': ['POST']},
1128 1134 requirements=URL_NAME_REQUIREMENTS)
1129 1135
1130 1136 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1131 1137 controller='forks', action='fork',
1132 1138 conditions={'function': check_repo},
1133 1139 requirements=URL_NAME_REQUIREMENTS)
1134 1140
1135 1141 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1136 1142 controller='forks', action='forks',
1137 1143 conditions={'function': check_repo},
1138 1144 requirements=URL_NAME_REQUIREMENTS)
1139 1145
1140 1146 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1141 1147 controller='followers', action='followers',
1142 1148 conditions={'function': check_repo},
1143 1149 requirements=URL_NAME_REQUIREMENTS)
1144 1150
1145 1151 # must be here for proper group/repo catching pattern
1146 1152 _connect_with_slash(
1147 1153 rmap, 'repo_group_home', '/{group_name}',
1148 1154 controller='home', action='index_repo_group',
1149 1155 conditions={'function': check_group},
1150 1156 requirements=URL_NAME_REQUIREMENTS)
1151 1157
1152 1158 # catch all, at the end
1153 1159 _connect_with_slash(
1154 1160 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1155 1161 controller='summary', action='index',
1156 1162 conditions={'function': check_repo},
1157 1163 requirements=URL_NAME_REQUIREMENTS)
1158 1164
1159 1165 return rmap
1160 1166
1161 1167
1162 1168 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1163 1169 """
1164 1170 Connect a route with an optional trailing slash in `path`.
1165 1171 """
1166 1172 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1167 1173 mapper.connect(name, path, *args, **kwargs)
@@ -1,1061 +1,1087 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Files controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import itertools
26 26 import logging
27 27 import os
28 28 import shutil
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 35
36 36 from rhodecode.controllers.utils import parse_path_ref
37 37 from rhodecode.lib import diffs, helpers as h, caches
38 38 from rhodecode.lib.compat import OrderedDict
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils import jsonify, action_logger
42 42 from rhodecode.lib.utils2 import (
43 43 convert_line_endings, detect_mode, safe_str, str2bool)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
46 46 from rhodecode.lib.base import BaseRepoController, render
47 47 from rhodecode.lib.vcs import path as vcspath
48 48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 49 from rhodecode.lib.vcs.conf import settings
50 50 from rhodecode.lib.vcs.exceptions import (
51 51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 53 NodeDoesNotExistError, CommitError, NodeError)
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55
56 56 from rhodecode.model.repo import RepoModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.db import Repository
59 59
60 60 from rhodecode.controllers.changeset import (
61 61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
62 62 from rhodecode.lib.exceptions import NonRelativePathError
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class FilesController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(FilesController, self).__before__()
71 71 c.cut_off_limit = self.cut_off_limit_file
72 72
73 73 def _get_default_encoding(self):
74 74 enc_list = getattr(c, 'default_encodings', [])
75 75 return enc_list[0] if enc_list else 'UTF-8'
76 76
77 77 def __get_commit_or_redirect(self, commit_id, repo_name,
78 78 redirect_after=True):
79 79 """
80 80 This is a safe way to get commit. If an error occurs it redirects to
81 81 tip with proper message
82 82
83 83 :param commit_id: id of commit to fetch
84 84 :param repo_name: repo name to redirect after
85 85 :param redirect_after: toggle redirection
86 86 """
87 87 try:
88 88 return c.rhodecode_repo.get_commit(commit_id)
89 89 except EmptyRepositoryError:
90 90 if not redirect_after:
91 91 return None
92 92 url_ = url('files_add_home',
93 93 repo_name=c.repo_name,
94 94 revision=0, f_path='', anchor='edit')
95 95 if h.HasRepoPermissionAny(
96 96 'repository.write', 'repository.admin')(c.repo_name):
97 97 add_new = h.link_to(
98 98 _('Click here to add a new file.'),
99 99 url_, class_="alert-link")
100 100 else:
101 101 add_new = ""
102 102 h.flash(h.literal(
103 103 _('There are no files yet. %s') % add_new), category='warning')
104 104 redirect(h.url('summary_home', repo_name=repo_name))
105 105 except (CommitDoesNotExistError, LookupError):
106 106 msg = _('No such commit exists for this repository')
107 107 h.flash(msg, category='error')
108 108 raise HTTPNotFound()
109 109 except RepositoryError as e:
110 110 h.flash(safe_str(e), category='error')
111 111 raise HTTPNotFound()
112 112
113 113 def __get_filenode_or_redirect(self, repo_name, commit, path):
114 114 """
115 115 Returns file_node, if error occurs or given path is directory,
116 116 it'll redirect to top level path
117 117
118 118 :param repo_name: repo_name
119 119 :param commit: given commit
120 120 :param path: path to lookup
121 121 """
122 122 try:
123 123 file_node = commit.get_node(path)
124 124 if file_node.is_dir():
125 125 raise RepositoryError('The given path is a directory')
126 126 except CommitDoesNotExistError:
127 127 msg = _('No such commit exists for this repository')
128 128 log.exception(msg)
129 129 h.flash(msg, category='error')
130 130 raise HTTPNotFound()
131 131 except RepositoryError as e:
132 132 h.flash(safe_str(e), category='error')
133 133 raise HTTPNotFound()
134 134
135 135 return file_node
136 136
137 137 def __get_tree_cache_manager(self, repo_name, namespace_type):
138 138 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
139 139 return caches.get_cache_manager('repo_cache_long', _namespace)
140 140
141 141 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
142 142 full_load=False, force=False):
143 143 def _cached_tree():
144 144 log.debug('Generating cached file tree for %s, %s, %s',
145 145 repo_name, commit_id, f_path)
146 146 c.full_load = full_load
147 147 return render('files/files_browser_tree.mako')
148 148
149 149 cache_manager = self.__get_tree_cache_manager(
150 150 repo_name, caches.FILE_TREE)
151 151
152 152 cache_key = caches.compute_key_from_params(
153 153 repo_name, commit_id, f_path)
154 154
155 155 if force:
156 156 # we want to force recompute of caches
157 157 cache_manager.remove_value(cache_key)
158 158
159 159 return cache_manager.get(cache_key, createfunc=_cached_tree)
160 160
161 161 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
162 162 def _cached_nodes():
163 163 log.debug('Generating cached nodelist for %s, %s, %s',
164 164 repo_name, commit_id, f_path)
165 165 _d, _f = ScmModel().get_nodes(
166 166 repo_name, commit_id, f_path, flat=False)
167 167 return _d + _f
168 168
169 169 cache_manager = self.__get_tree_cache_manager(
170 170 repo_name, caches.FILE_SEARCH_TREE_META)
171 171
172 172 cache_key = caches.compute_key_from_params(
173 173 repo_name, commit_id, f_path)
174 174 return cache_manager.get(cache_key, createfunc=_cached_nodes)
175 175
176 176 @LoginRequired()
177 177 @HasRepoPermissionAnyDecorator(
178 178 'repository.read', 'repository.write', 'repository.admin')
179 179 def index(
180 180 self, repo_name, revision, f_path, annotate=False, rendered=False):
181 181 commit_id = revision
182 182
183 183 # redirect to given commit_id from form if given
184 184 get_commit_id = request.GET.get('at_rev', None)
185 185 if get_commit_id:
186 186 self.__get_commit_or_redirect(get_commit_id, repo_name)
187 187
188 188 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
189 189 c.branch = request.GET.get('branch', None)
190 190 c.f_path = f_path
191 191 c.annotate = annotate
192 192 # default is false, but .rst/.md files later are autorendered, we can
193 193 # overwrite autorendering by setting this GET flag
194 194 c.renderer = rendered or not request.GET.get('no-render', False)
195 195
196 196 # prev link
197 197 try:
198 198 prev_commit = c.commit.prev(c.branch)
199 199 c.prev_commit = prev_commit
200 200 c.url_prev = url('files_home', repo_name=c.repo_name,
201 201 revision=prev_commit.raw_id, f_path=f_path)
202 202 if c.branch:
203 203 c.url_prev += '?branch=%s' % c.branch
204 204 except (CommitDoesNotExistError, VCSError):
205 205 c.url_prev = '#'
206 206 c.prev_commit = EmptyCommit()
207 207
208 208 # next link
209 209 try:
210 210 next_commit = c.commit.next(c.branch)
211 211 c.next_commit = next_commit
212 212 c.url_next = url('files_home', repo_name=c.repo_name,
213 213 revision=next_commit.raw_id, f_path=f_path)
214 214 if c.branch:
215 215 c.url_next += '?branch=%s' % c.branch
216 216 except (CommitDoesNotExistError, VCSError):
217 217 c.url_next = '#'
218 218 c.next_commit = EmptyCommit()
219 219
220 220 # files or dirs
221 221 try:
222 222 c.file = c.commit.get_node(f_path)
223 223 c.file_author = True
224 224 c.file_tree = ''
225 225 if c.file.is_file():
226 226 c.file_source_page = 'true'
227 227 c.file_last_commit = c.file.last_commit
228 228 if c.file.size < self.cut_off_limit_file:
229 229 if c.annotate: # annotation has precedence over renderer
230 230 c.annotated_lines = filenode_as_annotated_lines_tokens(
231 231 c.file
232 232 )
233 233 else:
234 234 c.renderer = (
235 235 c.renderer and h.renderer_from_filename(c.file.path)
236 236 )
237 237 if not c.renderer:
238 238 c.lines = filenode_as_lines_tokens(c.file)
239 239
240 240 c.on_branch_head = self._is_valid_head(
241 241 commit_id, c.rhodecode_repo)
242 242 c.branch_or_raw_id = c.commit.branch or c.commit.raw_id
243 243
244 244 author = c.file_last_commit.author
245 245 c.authors = [(h.email(author),
246 246 h.person(author, 'username_or_name_or_email'))]
247 247 else:
248 248 c.file_source_page = 'false'
249 249 c.authors = []
250 250 c.file_tree = self._get_tree_at_commit(
251 251 repo_name, c.commit.raw_id, f_path)
252 252
253 253 except RepositoryError as e:
254 254 h.flash(safe_str(e), category='error')
255 255 raise HTTPNotFound()
256 256
257 257 if request.environ.get('HTTP_X_PJAX'):
258 258 return render('files/files_pjax.mako')
259 259
260 260 return render('files/files.mako')
261 261
262 262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
265 def annotate_previous(self, repo_name, revision, f_path):
266
267 commit_id = revision
268 commit = self.__get_commit_or_redirect(commit_id, repo_name)
269 prev_commit_id = commit.raw_id
270
271 f_path = f_path
272 is_file = False
273 try:
274 _file = commit.get_node(f_path)
275 is_file = _file.is_file()
276 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
277 pass
278
279 if is_file:
280 history = commit.get_file_history(f_path)
281 prev_commit_id = history[1].raw_id \
282 if len(history) > 1 else prev_commit_id
283
284 return redirect(h.url(
285 'files_annotate_home', repo_name=repo_name,
286 revision=prev_commit_id, f_path=f_path))
287
288 @LoginRequired()
263 289 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
264 290 'repository.admin')
265 291 @jsonify
266 292 def history(self, repo_name, revision, f_path):
267 293 commit = self.__get_commit_or_redirect(revision, repo_name)
268 294 f_path = f_path
269 295 _file = commit.get_node(f_path)
270 296 if _file.is_file():
271 297 file_history, _hist = self._get_node_history(commit, f_path)
272 298
273 299 res = []
274 300 for obj in file_history:
275 301 res.append({
276 302 'text': obj[1],
277 303 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
278 304 })
279 305
280 306 data = {
281 307 'more': False,
282 308 'results': res
283 309 }
284 310 return data
285 311
286 312 @LoginRequired()
287 313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
288 314 'repository.admin')
289 315 def authors(self, repo_name, revision, f_path):
290 316 commit = self.__get_commit_or_redirect(revision, repo_name)
291 317 file_node = commit.get_node(f_path)
292 318 if file_node.is_file():
293 319 c.file_last_commit = file_node.last_commit
294 320 if request.GET.get('annotate') == '1':
295 321 # use _hist from annotation if annotation mode is on
296 322 commit_ids = set(x[1] for x in file_node.annotate)
297 323 _hist = (
298 324 c.rhodecode_repo.get_commit(commit_id)
299 325 for commit_id in commit_ids)
300 326 else:
301 327 _f_history, _hist = self._get_node_history(commit, f_path)
302 328 c.file_author = False
303 329 c.authors = []
304 330 for author in set(commit.author for commit in _hist):
305 331 c.authors.append((
306 332 h.email(author),
307 333 h.person(author, 'username_or_name_or_email')))
308 334 return render('files/file_authors_box.mako')
309 335
310 336 @LoginRequired()
311 337 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 338 'repository.admin')
313 339 def rawfile(self, repo_name, revision, f_path):
314 340 """
315 341 Action for download as raw
316 342 """
317 343 commit = self.__get_commit_or_redirect(revision, repo_name)
318 344 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
319 345
320 346 response.content_disposition = 'attachment; filename=%s' % \
321 347 safe_str(f_path.split(Repository.NAME_SEP)[-1])
322 348
323 349 response.content_type = file_node.mimetype
324 350 charset = self._get_default_encoding()
325 351 if charset:
326 352 response.charset = charset
327 353
328 354 return file_node.content
329 355
330 356 @LoginRequired()
331 357 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 358 'repository.admin')
333 359 def raw(self, repo_name, revision, f_path):
334 360 """
335 361 Action for show as raw, some mimetypes are "rendered",
336 362 those include images, icons.
337 363 """
338 364 commit = self.__get_commit_or_redirect(revision, repo_name)
339 365 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
340 366
341 367 raw_mimetype_mapping = {
342 368 # map original mimetype to a mimetype used for "show as raw"
343 369 # you can also provide a content-disposition to override the
344 370 # default "attachment" disposition.
345 371 # orig_type: (new_type, new_dispo)
346 372
347 373 # show images inline:
348 374 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
349 375 # for example render an SVG with javascript inside or even render
350 376 # HTML.
351 377 'image/x-icon': ('image/x-icon', 'inline'),
352 378 'image/png': ('image/png', 'inline'),
353 379 'image/gif': ('image/gif', 'inline'),
354 380 'image/jpeg': ('image/jpeg', 'inline'),
355 381 }
356 382
357 383 mimetype = file_node.mimetype
358 384 try:
359 385 mimetype, dispo = raw_mimetype_mapping[mimetype]
360 386 except KeyError:
361 387 # we don't know anything special about this, handle it safely
362 388 if file_node.is_binary:
363 389 # do same as download raw for binary files
364 390 mimetype, dispo = 'application/octet-stream', 'attachment'
365 391 else:
366 392 # do not just use the original mimetype, but force text/plain,
367 393 # otherwise it would serve text/html and that might be unsafe.
368 394 # Note: underlying vcs library fakes text/plain mimetype if the
369 395 # mimetype can not be determined and it thinks it is not
370 396 # binary.This might lead to erroneous text display in some
371 397 # cases, but helps in other cases, like with text files
372 398 # without extension.
373 399 mimetype, dispo = 'text/plain', 'inline'
374 400
375 401 if dispo == 'attachment':
376 402 dispo = 'attachment; filename=%s' % safe_str(
377 403 f_path.split(os.sep)[-1])
378 404
379 405 response.content_disposition = dispo
380 406 response.content_type = mimetype
381 407 charset = self._get_default_encoding()
382 408 if charset:
383 409 response.charset = charset
384 410 return file_node.content
385 411
386 412 @CSRFRequired()
387 413 @LoginRequired()
388 414 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
389 415 def delete(self, repo_name, revision, f_path):
390 416 commit_id = revision
391 417
392 418 repo = c.rhodecode_db_repo
393 419 if repo.enable_locking and repo.locked[0]:
394 420 h.flash(_('This repository has been locked by %s on %s')
395 421 % (h.person_by_id(repo.locked[0]),
396 422 h.format_date(h.time_to_datetime(repo.locked[1]))),
397 423 'warning')
398 424 return redirect(h.url('files_home',
399 425 repo_name=repo_name, revision='tip'))
400 426
401 427 if not self._is_valid_head(commit_id, repo.scm_instance()):
402 428 h.flash(_('You can only delete files with revision '
403 429 'being a valid branch '), category='warning')
404 430 return redirect(h.url('files_home',
405 431 repo_name=repo_name, revision='tip',
406 432 f_path=f_path))
407 433
408 434 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
409 435 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
410 436
411 437 c.default_message = _(
412 438 'Deleted file %s via RhodeCode Enterprise') % (f_path)
413 439 c.f_path = f_path
414 440 node_path = f_path
415 441 author = c.rhodecode_user.full_contact
416 442 message = request.POST.get('message') or c.default_message
417 443 try:
418 444 nodes = {
419 445 node_path: {
420 446 'content': ''
421 447 }
422 448 }
423 449 self.scm_model.delete_nodes(
424 450 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
425 451 message=message,
426 452 nodes=nodes,
427 453 parent_commit=c.commit,
428 454 author=author,
429 455 )
430 456
431 457 h.flash(_('Successfully deleted file %s') % f_path,
432 458 category='success')
433 459 except Exception:
434 460 msg = _('Error occurred during commit')
435 461 log.exception(msg)
436 462 h.flash(msg, category='error')
437 463 return redirect(url('changeset_home',
438 464 repo_name=c.repo_name, revision='tip'))
439 465
440 466 @LoginRequired()
441 467 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
442 468 def delete_home(self, repo_name, revision, f_path):
443 469 commit_id = revision
444 470
445 471 repo = c.rhodecode_db_repo
446 472 if repo.enable_locking and repo.locked[0]:
447 473 h.flash(_('This repository has been locked by %s on %s')
448 474 % (h.person_by_id(repo.locked[0]),
449 475 h.format_date(h.time_to_datetime(repo.locked[1]))),
450 476 'warning')
451 477 return redirect(h.url('files_home',
452 478 repo_name=repo_name, revision='tip'))
453 479
454 480 if not self._is_valid_head(commit_id, repo.scm_instance()):
455 481 h.flash(_('You can only delete files with revision '
456 482 'being a valid branch '), category='warning')
457 483 return redirect(h.url('files_home',
458 484 repo_name=repo_name, revision='tip',
459 485 f_path=f_path))
460 486
461 487 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
462 488 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
463 489
464 490 c.default_message = _(
465 491 'Deleted file %s via RhodeCode Enterprise') % (f_path)
466 492 c.f_path = f_path
467 493
468 494 return render('files/files_delete.mako')
469 495
470 496 @CSRFRequired()
471 497 @LoginRequired()
472 498 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
473 499 def edit(self, repo_name, revision, f_path):
474 500 commit_id = revision
475 501
476 502 repo = c.rhodecode_db_repo
477 503 if repo.enable_locking and repo.locked[0]:
478 504 h.flash(_('This repository has been locked by %s on %s')
479 505 % (h.person_by_id(repo.locked[0]),
480 506 h.format_date(h.time_to_datetime(repo.locked[1]))),
481 507 'warning')
482 508 return redirect(h.url('files_home',
483 509 repo_name=repo_name, revision='tip'))
484 510
485 511 if not self._is_valid_head(commit_id, repo.scm_instance()):
486 512 h.flash(_('You can only edit files with revision '
487 513 'being a valid branch '), category='warning')
488 514 return redirect(h.url('files_home',
489 515 repo_name=repo_name, revision='tip',
490 516 f_path=f_path))
491 517
492 518 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
493 519 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
494 520
495 521 if c.file.is_binary:
496 522 return redirect(url('files_home', repo_name=c.repo_name,
497 523 revision=c.commit.raw_id, f_path=f_path))
498 524 c.default_message = _(
499 525 'Edited file %s via RhodeCode Enterprise') % (f_path)
500 526 c.f_path = f_path
501 527 old_content = c.file.content
502 528 sl = old_content.splitlines(1)
503 529 first_line = sl[0] if sl else ''
504 530
505 531 # modes: 0 - Unix, 1 - Mac, 2 - DOS
506 532 mode = detect_mode(first_line, 0)
507 533 content = convert_line_endings(request.POST.get('content', ''), mode)
508 534
509 535 message = request.POST.get('message') or c.default_message
510 536 org_f_path = c.file.unicode_path
511 537 filename = request.POST['filename']
512 538 org_filename = c.file.name
513 539
514 540 if content == old_content and filename == org_filename:
515 541 h.flash(_('No changes'), category='warning')
516 542 return redirect(url('changeset_home', repo_name=c.repo_name,
517 543 revision='tip'))
518 544 try:
519 545 mapping = {
520 546 org_f_path: {
521 547 'org_filename': org_f_path,
522 548 'filename': os.path.join(c.file.dir_path, filename),
523 549 'content': content,
524 550 'lexer': '',
525 551 'op': 'mod',
526 552 }
527 553 }
528 554
529 555 ScmModel().update_nodes(
530 556 user=c.rhodecode_user.user_id,
531 557 repo=c.rhodecode_db_repo,
532 558 message=message,
533 559 nodes=mapping,
534 560 parent_commit=c.commit,
535 561 )
536 562
537 563 h.flash(_('Successfully committed to %s') % f_path,
538 564 category='success')
539 565 except Exception:
540 566 msg = _('Error occurred during commit')
541 567 log.exception(msg)
542 568 h.flash(msg, category='error')
543 569 return redirect(url('changeset_home',
544 570 repo_name=c.repo_name, revision='tip'))
545 571
546 572 @LoginRequired()
547 573 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
548 574 def edit_home(self, repo_name, revision, f_path):
549 575 commit_id = revision
550 576
551 577 repo = c.rhodecode_db_repo
552 578 if repo.enable_locking and repo.locked[0]:
553 579 h.flash(_('This repository has been locked by %s on %s')
554 580 % (h.person_by_id(repo.locked[0]),
555 581 h.format_date(h.time_to_datetime(repo.locked[1]))),
556 582 'warning')
557 583 return redirect(h.url('files_home',
558 584 repo_name=repo_name, revision='tip'))
559 585
560 586 if not self._is_valid_head(commit_id, repo.scm_instance()):
561 587 h.flash(_('You can only edit files with revision '
562 588 'being a valid branch '), category='warning')
563 589 return redirect(h.url('files_home',
564 590 repo_name=repo_name, revision='tip',
565 591 f_path=f_path))
566 592
567 593 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
568 594 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
569 595
570 596 if c.file.is_binary:
571 597 return redirect(url('files_home', repo_name=c.repo_name,
572 598 revision=c.commit.raw_id, f_path=f_path))
573 599 c.default_message = _(
574 600 'Edited file %s via RhodeCode Enterprise') % (f_path)
575 601 c.f_path = f_path
576 602
577 603 return render('files/files_edit.mako')
578 604
579 605 def _is_valid_head(self, commit_id, repo):
580 606 # check if commit is a branch identifier- basically we cannot
581 607 # create multiple heads via file editing
582 608 valid_heads = repo.branches.keys() + repo.branches.values()
583 609
584 610 if h.is_svn(repo) and not repo.is_empty():
585 611 # Note: Subversion only has one head, we add it here in case there
586 612 # is no branch matched.
587 613 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
588 614
589 615 # check if commit is a branch name or branch hash
590 616 return commit_id in valid_heads
591 617
592 618 @CSRFRequired()
593 619 @LoginRequired()
594 620 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
595 621 def add(self, repo_name, revision, f_path):
596 622 repo = Repository.get_by_repo_name(repo_name)
597 623 if repo.enable_locking and repo.locked[0]:
598 624 h.flash(_('This repository has been locked by %s on %s')
599 625 % (h.person_by_id(repo.locked[0]),
600 626 h.format_date(h.time_to_datetime(repo.locked[1]))),
601 627 'warning')
602 628 return redirect(h.url('files_home',
603 629 repo_name=repo_name, revision='tip'))
604 630
605 631 r_post = request.POST
606 632
607 633 c.commit = self.__get_commit_or_redirect(
608 634 revision, repo_name, redirect_after=False)
609 635 if c.commit is None:
610 636 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
611 637 c.default_message = (_('Added file via RhodeCode Enterprise'))
612 638 c.f_path = f_path
613 639 unix_mode = 0
614 640 content = convert_line_endings(r_post.get('content', ''), unix_mode)
615 641
616 642 message = r_post.get('message') or c.default_message
617 643 filename = r_post.get('filename')
618 644 location = r_post.get('location', '') # dir location
619 645 file_obj = r_post.get('upload_file', None)
620 646
621 647 if file_obj is not None and hasattr(file_obj, 'filename'):
622 648 filename = file_obj.filename
623 649 content = file_obj.file
624 650
625 651 if hasattr(content, 'file'):
626 652 # non posix systems store real file under file attr
627 653 content = content.file
628 654
629 655 # If there's no commit, redirect to repo summary
630 656 if type(c.commit) is EmptyCommit:
631 657 redirect_url = "summary_home"
632 658 else:
633 659 redirect_url = "changeset_home"
634 660
635 661 if not filename:
636 662 h.flash(_('No filename'), category='warning')
637 663 return redirect(url(redirect_url, repo_name=c.repo_name,
638 664 revision='tip'))
639 665
640 666 # extract the location from filename,
641 667 # allows using foo/bar.txt syntax to create subdirectories
642 668 subdir_loc = filename.rsplit('/', 1)
643 669 if len(subdir_loc) == 2:
644 670 location = os.path.join(location, subdir_loc[0])
645 671
646 672 # strip all crap out of file, just leave the basename
647 673 filename = os.path.basename(filename)
648 674 node_path = os.path.join(location, filename)
649 675 author = c.rhodecode_user.full_contact
650 676
651 677 try:
652 678 nodes = {
653 679 node_path: {
654 680 'content': content
655 681 }
656 682 }
657 683 self.scm_model.create_nodes(
658 684 user=c.rhodecode_user.user_id,
659 685 repo=c.rhodecode_db_repo,
660 686 message=message,
661 687 nodes=nodes,
662 688 parent_commit=c.commit,
663 689 author=author,
664 690 )
665 691
666 692 h.flash(_('Successfully committed to %s') % node_path,
667 693 category='success')
668 694 except NonRelativePathError as e:
669 695 h.flash(_(
670 696 'The location specified must be a relative path and must not '
671 697 'contain .. in the path'), category='warning')
672 698 return redirect(url('changeset_home', repo_name=c.repo_name,
673 699 revision='tip'))
674 700 except (NodeError, NodeAlreadyExistsError) as e:
675 701 h.flash(_(e), category='error')
676 702 except Exception:
677 703 msg = _('Error occurred during commit')
678 704 log.exception(msg)
679 705 h.flash(msg, category='error')
680 706 return redirect(url('changeset_home',
681 707 repo_name=c.repo_name, revision='tip'))
682 708
683 709 @LoginRequired()
684 710 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
685 711 def add_home(self, repo_name, revision, f_path):
686 712
687 713 repo = Repository.get_by_repo_name(repo_name)
688 714 if repo.enable_locking and repo.locked[0]:
689 715 h.flash(_('This repository has been locked by %s on %s')
690 716 % (h.person_by_id(repo.locked[0]),
691 717 h.format_date(h.time_to_datetime(repo.locked[1]))),
692 718 'warning')
693 719 return redirect(h.url('files_home',
694 720 repo_name=repo_name, revision='tip'))
695 721
696 722 c.commit = self.__get_commit_or_redirect(
697 723 revision, repo_name, redirect_after=False)
698 724 if c.commit is None:
699 725 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
700 726 c.default_message = (_('Added file via RhodeCode Enterprise'))
701 727 c.f_path = f_path
702 728
703 729 return render('files/files_add.mako')
704 730
705 731 @LoginRequired()
706 732 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
707 733 'repository.admin')
708 734 def archivefile(self, repo_name, fname):
709 735 fileformat = None
710 736 commit_id = None
711 737 ext = None
712 738 subrepos = request.GET.get('subrepos') == 'true'
713 739
714 740 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
715 741 archive_spec = fname.split(ext_data[1])
716 742 if len(archive_spec) == 2 and archive_spec[1] == '':
717 743 fileformat = a_type or ext_data[1]
718 744 commit_id = archive_spec[0]
719 745 ext = ext_data[1]
720 746
721 747 dbrepo = RepoModel().get_by_repo_name(repo_name)
722 748 if not dbrepo.enable_downloads:
723 749 return _('Downloads disabled')
724 750
725 751 try:
726 752 commit = c.rhodecode_repo.get_commit(commit_id)
727 753 content_type = settings.ARCHIVE_SPECS[fileformat][0]
728 754 except CommitDoesNotExistError:
729 755 return _('Unknown revision %s') % commit_id
730 756 except EmptyRepositoryError:
731 757 return _('Empty repository')
732 758 except KeyError:
733 759 return _('Unknown archive type')
734 760
735 761 # archive cache
736 762 from rhodecode import CONFIG
737 763
738 764 archive_name = '%s-%s%s%s' % (
739 765 safe_str(repo_name.replace('/', '_')),
740 766 '-sub' if subrepos else '',
741 767 safe_str(commit.short_id), ext)
742 768
743 769 use_cached_archive = False
744 770 archive_cache_enabled = CONFIG.get(
745 771 'archive_cache_dir') and not request.GET.get('no_cache')
746 772
747 773 if archive_cache_enabled:
748 774 # check if we it's ok to write
749 775 if not os.path.isdir(CONFIG['archive_cache_dir']):
750 776 os.makedirs(CONFIG['archive_cache_dir'])
751 777 cached_archive_path = os.path.join(
752 778 CONFIG['archive_cache_dir'], archive_name)
753 779 if os.path.isfile(cached_archive_path):
754 780 log.debug('Found cached archive in %s', cached_archive_path)
755 781 fd, archive = None, cached_archive_path
756 782 use_cached_archive = True
757 783 else:
758 784 log.debug('Archive %s is not yet cached', archive_name)
759 785
760 786 if not use_cached_archive:
761 787 # generate new archive
762 788 fd, archive = tempfile.mkstemp()
763 789 log.debug('Creating new temp archive in %s' % (archive,))
764 790 try:
765 791 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
766 792 except ImproperArchiveTypeError:
767 793 return _('Unknown archive type')
768 794 if archive_cache_enabled:
769 795 # if we generated the archive and we have cache enabled
770 796 # let's use this for future
771 797 log.debug('Storing new archive in %s' % (cached_archive_path,))
772 798 shutil.move(archive, cached_archive_path)
773 799 archive = cached_archive_path
774 800
775 801 def get_chunked_archive(archive):
776 802 with open(archive, 'rb') as stream:
777 803 while True:
778 804 data = stream.read(16 * 1024)
779 805 if not data:
780 806 if fd: # fd means we used temporary file
781 807 os.close(fd)
782 808 if not archive_cache_enabled:
783 809 log.debug('Destroying temp archive %s', archive)
784 810 os.remove(archive)
785 811 break
786 812 yield data
787 813
788 814 # store download action
789 815 action_logger(user=c.rhodecode_user,
790 816 action='user_downloaded_archive:%s' % archive_name,
791 817 repo=repo_name, ipaddr=self.ip_addr, commit=True)
792 818 response.content_disposition = str(
793 819 'attachment; filename=%s' % archive_name)
794 820 response.content_type = str(content_type)
795 821
796 822 return get_chunked_archive(archive)
797 823
798 824 @LoginRequired()
799 825 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
800 826 'repository.admin')
801 827 def diff(self, repo_name, f_path):
802 828
803 829 c.action = request.GET.get('diff')
804 830 diff1 = request.GET.get('diff1', '')
805 831 diff2 = request.GET.get('diff2', '')
806 832
807 833 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
808 834
809 835 ignore_whitespace = str2bool(request.GET.get('ignorews'))
810 836 line_context = request.GET.get('context', 3)
811 837
812 838 if not any((diff1, diff2)):
813 839 h.flash(
814 840 'Need query parameter "diff1" or "diff2" to generate a diff.',
815 841 category='error')
816 842 raise HTTPBadRequest()
817 843
818 844 if c.action not in ['download', 'raw']:
819 845 # redirect to new view if we render diff
820 846 return redirect(
821 847 url('compare_url', repo_name=repo_name,
822 848 source_ref_type='rev',
823 849 source_ref=diff1,
824 850 target_repo=c.repo_name,
825 851 target_ref_type='rev',
826 852 target_ref=diff2,
827 853 f_path=f_path))
828 854
829 855 try:
830 856 node1 = self._get_file_node(diff1, path1)
831 857 node2 = self._get_file_node(diff2, f_path)
832 858 except (RepositoryError, NodeError):
833 859 log.exception("Exception while trying to get node from repository")
834 860 return redirect(url(
835 861 'files_home', repo_name=c.repo_name, f_path=f_path))
836 862
837 863 if all(isinstance(node.commit, EmptyCommit)
838 864 for node in (node1, node2)):
839 865 raise HTTPNotFound
840 866
841 867 c.commit_1 = node1.commit
842 868 c.commit_2 = node2.commit
843 869
844 870 if c.action == 'download':
845 871 _diff = diffs.get_gitdiff(node1, node2,
846 872 ignore_whitespace=ignore_whitespace,
847 873 context=line_context)
848 874 diff = diffs.DiffProcessor(_diff, format='gitdiff')
849 875
850 876 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
851 877 response.content_type = 'text/plain'
852 878 response.content_disposition = (
853 879 'attachment; filename=%s' % (diff_name,)
854 880 )
855 881 charset = self._get_default_encoding()
856 882 if charset:
857 883 response.charset = charset
858 884 return diff.as_raw()
859 885
860 886 elif c.action == 'raw':
861 887 _diff = diffs.get_gitdiff(node1, node2,
862 888 ignore_whitespace=ignore_whitespace,
863 889 context=line_context)
864 890 diff = diffs.DiffProcessor(_diff, format='gitdiff')
865 891 response.content_type = 'text/plain'
866 892 charset = self._get_default_encoding()
867 893 if charset:
868 894 response.charset = charset
869 895 return diff.as_raw()
870 896
871 897 else:
872 898 return redirect(
873 899 url('compare_url', repo_name=repo_name,
874 900 source_ref_type='rev',
875 901 source_ref=diff1,
876 902 target_repo=c.repo_name,
877 903 target_ref_type='rev',
878 904 target_ref=diff2,
879 905 f_path=f_path))
880 906
881 907 @LoginRequired()
882 908 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
883 909 'repository.admin')
884 910 def diff_2way(self, repo_name, f_path):
885 911 """
886 912 Kept only to make OLD links work
887 913 """
888 914 diff1 = request.GET.get('diff1', '')
889 915 diff2 = request.GET.get('diff2', '')
890 916
891 917 if not any((diff1, diff2)):
892 918 h.flash(
893 919 'Need query parameter "diff1" or "diff2" to generate a diff.',
894 920 category='error')
895 921 raise HTTPBadRequest()
896 922
897 923 return redirect(
898 924 url('compare_url', repo_name=repo_name,
899 925 source_ref_type='rev',
900 926 source_ref=diff1,
901 927 target_repo=c.repo_name,
902 928 target_ref_type='rev',
903 929 target_ref=diff2,
904 930 f_path=f_path,
905 931 diffmode='sideside'))
906 932
907 933 def _get_file_node(self, commit_id, f_path):
908 934 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
909 935 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
910 936 try:
911 937 node = commit.get_node(f_path)
912 938 if node.is_dir():
913 939 raise NodeError('%s path is a %s not a file'
914 940 % (node, type(node)))
915 941 except NodeDoesNotExistError:
916 942 commit = EmptyCommit(
917 943 commit_id=commit_id,
918 944 idx=commit.idx,
919 945 repo=commit.repository,
920 946 alias=commit.repository.alias,
921 947 message=commit.message,
922 948 author=commit.author,
923 949 date=commit.date)
924 950 node = FileNode(f_path, '', commit=commit)
925 951 else:
926 952 commit = EmptyCommit(
927 953 repo=c.rhodecode_repo,
928 954 alias=c.rhodecode_repo.alias)
929 955 node = FileNode(f_path, '', commit=commit)
930 956 return node
931 957
932 958 def _get_node_history(self, commit, f_path, commits=None):
933 959 """
934 960 get commit history for given node
935 961
936 962 :param commit: commit to calculate history
937 963 :param f_path: path for node to calculate history for
938 964 :param commits: if passed don't calculate history and take
939 965 commits defined in this list
940 966 """
941 967 # calculate history based on tip
942 968 tip = c.rhodecode_repo.get_commit()
943 969 if commits is None:
944 970 pre_load = ["author", "branch"]
945 971 try:
946 972 commits = tip.get_file_history(f_path, pre_load=pre_load)
947 973 except (NodeDoesNotExistError, CommitError):
948 974 # this node is not present at tip!
949 975 commits = commit.get_file_history(f_path, pre_load=pre_load)
950 976
951 977 history = []
952 978 commits_group = ([], _("Changesets"))
953 979 for commit in commits:
954 980 branch = ' (%s)' % commit.branch if commit.branch else ''
955 981 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
956 982 commits_group[0].append((commit.raw_id, n_desc,))
957 983 history.append(commits_group)
958 984
959 985 symbolic_reference = self._symbolic_reference
960 986
961 987 if c.rhodecode_repo.alias == 'svn':
962 988 adjusted_f_path = self._adjust_file_path_for_svn(
963 989 f_path, c.rhodecode_repo)
964 990 if adjusted_f_path != f_path:
965 991 log.debug(
966 992 'Recognized svn tag or branch in file "%s", using svn '
967 993 'specific symbolic references', f_path)
968 994 f_path = adjusted_f_path
969 995 symbolic_reference = self._symbolic_reference_svn
970 996
971 997 branches = self._create_references(
972 998 c.rhodecode_repo.branches, symbolic_reference, f_path)
973 999 branches_group = (branches, _("Branches"))
974 1000
975 1001 tags = self._create_references(
976 1002 c.rhodecode_repo.tags, symbolic_reference, f_path)
977 1003 tags_group = (tags, _("Tags"))
978 1004
979 1005 history.append(branches_group)
980 1006 history.append(tags_group)
981 1007
982 1008 return history, commits
983 1009
984 1010 def _adjust_file_path_for_svn(self, f_path, repo):
985 1011 """
986 1012 Computes the relative path of `f_path`.
987 1013
988 1014 This is mainly based on prefix matching of the recognized tags and
989 1015 branches in the underlying repository.
990 1016 """
991 1017 tags_and_branches = itertools.chain(
992 1018 repo.branches.iterkeys(),
993 1019 repo.tags.iterkeys())
994 1020 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
995 1021
996 1022 for name in tags_and_branches:
997 1023 if f_path.startswith(name + '/'):
998 1024 f_path = vcspath.relpath(f_path, name)
999 1025 break
1000 1026 return f_path
1001 1027
1002 1028 def _create_references(
1003 1029 self, branches_or_tags, symbolic_reference, f_path):
1004 1030 items = []
1005 1031 for name, commit_id in branches_or_tags.items():
1006 1032 sym_ref = symbolic_reference(commit_id, name, f_path)
1007 1033 items.append((sym_ref, name))
1008 1034 return items
1009 1035
1010 1036 def _symbolic_reference(self, commit_id, name, f_path):
1011 1037 return commit_id
1012 1038
1013 1039 def _symbolic_reference_svn(self, commit_id, name, f_path):
1014 1040 new_f_path = vcspath.join(name, f_path)
1015 1041 return u'%s@%s' % (new_f_path, commit_id)
1016 1042
1017 1043 @LoginRequired()
1018 1044 @XHRRequired()
1019 1045 @HasRepoPermissionAnyDecorator(
1020 1046 'repository.read', 'repository.write', 'repository.admin')
1021 1047 @jsonify
1022 1048 def nodelist(self, repo_name, revision, f_path):
1023 1049 commit = self.__get_commit_or_redirect(revision, repo_name)
1024 1050
1025 1051 metadata = self._get_nodelist_at_commit(
1026 1052 repo_name, commit.raw_id, f_path)
1027 1053 return {'nodes': metadata}
1028 1054
1029 1055 @LoginRequired()
1030 1056 @XHRRequired()
1031 1057 @HasRepoPermissionAnyDecorator(
1032 1058 'repository.read', 'repository.write', 'repository.admin')
1033 1059 def nodetree_full(self, repo_name, commit_id, f_path):
1034 1060 """
1035 1061 Returns rendered html of file tree that contains commit date,
1036 1062 author, revision for the specified combination of
1037 1063 repo, commit_id and file path
1038 1064
1039 1065 :param repo_name: name of the repository
1040 1066 :param commit_id: commit_id of file tree
1041 1067 :param f_path: file path of the requested directory
1042 1068 """
1043 1069
1044 1070 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1045 1071 try:
1046 1072 dir_node = commit.get_node(f_path)
1047 1073 except RepositoryError as e:
1048 1074 return 'error {}'.format(safe_str(e))
1049 1075
1050 1076 if dir_node.is_file():
1051 1077 return ''
1052 1078
1053 1079 c.file = dir_node
1054 1080 c.commit = commit
1055 1081
1056 1082 # using force=True here, make a little trick. We flush the cache and
1057 1083 # compute it using the same key as without full_load, so the fully
1058 1084 # loaded cached tree is now returned instead of partial
1059 1085 return self._get_tree_at_commit(
1060 1086 repo_name, commit.raw_id, dir_node.path, full_load=True,
1061 1087 force=True)
@@ -1,1135 +1,1136 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21 .compare_view_files {
22 22
23 23 .diff-container {
24 24
25 25 .diffblock {
26 26 margin-bottom: 0;
27 27 }
28 28 }
29 29 }
30 30
31 31 div.diffblock .sidebyside {
32 32 background: #ffffff;
33 33 }
34 34
35 35 div.diffblock {
36 36 overflow-x: auto;
37 37 overflow-y: hidden;
38 38 clear: both;
39 39 padding: 0px;
40 40 background: @grey6;
41 41 border: @border-thickness solid @grey5;
42 42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 43 border-radius: @border-radius @border-radius 0px 0px;
44 44
45 45
46 46 .comments-number {
47 47 float: right;
48 48 }
49 49
50 50 // BEGIN CODE-HEADER STYLES
51 51
52 52 .code-header {
53 53 background: @grey6;
54 54 padding: 10px 0 10px 0;
55 55 height: auto;
56 56 width: 100%;
57 57
58 58 .hash {
59 59 float: left;
60 60 padding: 2px 0 0 2px;
61 61 }
62 62
63 63 .date {
64 64 float: left;
65 65 text-transform: uppercase;
66 66 padding: 4px 0px 0px 2px;
67 67 }
68 68
69 69 div {
70 70 margin-left: 4px;
71 71 }
72 72
73 73 div.compare_header {
74 74 min-height: 40px;
75 75 margin: 0;
76 76 padding: 0 @padding;
77 77
78 78 .drop-menu {
79 79 float:left;
80 80 display: block;
81 81 margin:0 0 @padding 0;
82 82 }
83 83
84 84 .compare-label {
85 85 float: left;
86 86 clear: both;
87 87 display: inline-block;
88 88 min-width: 5em;
89 89 margin: 0;
90 90 padding: @button-padding @button-padding @button-padding 0;
91 91 font-family: @text-semibold;
92 92 }
93 93
94 94 .compare-buttons {
95 95 float: left;
96 96 margin: 0;
97 97 padding: 0 0 @padding;
98 98
99 99 .btn {
100 100 margin: 0 @padding 0 0;
101 101 }
102 102 }
103 103 }
104 104
105 105 }
106 106
107 107 .parents {
108 108 float: left;
109 109 width: 100px;
110 110 font-weight: 400;
111 111 vertical-align: middle;
112 112 padding: 0px 2px 0px 2px;
113 113 background-color: @grey6;
114 114
115 115 #parent_link {
116 116 margin: 00px 2px;
117 117
118 118 &.double {
119 119 margin: 0px 2px;
120 120 }
121 121
122 122 &.disabled{
123 123 margin-right: @padding;
124 124 }
125 125 }
126 126 }
127 127
128 128 .children {
129 129 float: right;
130 130 width: 100px;
131 131 font-weight: 400;
132 132 vertical-align: middle;
133 133 text-align: right;
134 134 padding: 0px 2px 0px 2px;
135 135 background-color: @grey6;
136 136
137 137 #child_link {
138 138 margin: 0px 2px;
139 139
140 140 &.double {
141 141 margin: 0px 2px;
142 142 }
143 143
144 144 &.disabled{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148 }
149 149
150 150 .changeset_header {
151 151 height: 16px;
152 152
153 153 & > div{
154 154 margin-right: @padding;
155 155 }
156 156 }
157 157
158 158 .changeset_file {
159 159 text-align: left;
160 160 float: left;
161 161 padding: 0;
162 162
163 163 a{
164 164 display: inline-block;
165 165 margin-right: 0.5em;
166 166 }
167 167
168 168 #selected_mode{
169 169 margin-left: 0;
170 170 }
171 171 }
172 172
173 173 .diff-menu-wrapper {
174 174 float: left;
175 175 }
176 176
177 177 .diff-menu {
178 178 position: absolute;
179 179 background: none repeat scroll 0 0 #FFFFFF;
180 180 border-color: #003367 @grey3 @grey3;
181 181 border-right: 1px solid @grey3;
182 182 border-style: solid solid solid;
183 183 border-width: @border-thickness;
184 184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 185 margin-top: 5px;
186 186 margin-left: 1px;
187 187 }
188 188
189 189 .diff-actions, .editor-actions {
190 190 float: left;
191 191
192 192 input{
193 193 margin: 0 0.5em 0 0;
194 194 }
195 195 }
196 196
197 197 // END CODE-HEADER STYLES
198 198
199 199 // BEGIN CODE-BODY STYLES
200 200
201 201 .code-body {
202 202 background: white;
203 203 padding: 0;
204 204 background-color: #ffffff;
205 205 position: relative;
206 206 max-width: none;
207 207 box-sizing: border-box;
208 208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 209 // to have the intended size and to scroll. Should be simplified.
210 210 width: 100%;
211 211 overflow-x: auto;
212 212 }
213 213
214 214 pre.raw {
215 215 background: white;
216 216 color: @grey1;
217 217 }
218 218 // END CODE-BODY STYLES
219 219
220 220 }
221 221
222 222
223 223 table.code-difftable {
224 224 border-collapse: collapse;
225 225 width: 99%;
226 226 border-radius: 0px !important;
227 227
228 228 td {
229 229 padding: 0 !important;
230 230 background: none !important;
231 231 border: 0 !important;
232 232 }
233 233
234 234 .context {
235 235 background: none repeat scroll 0 0 #DDE7EF;
236 236 }
237 237
238 238 .add {
239 239 background: none repeat scroll 0 0 #DDFFDD;
240 240
241 241 ins {
242 242 background: none repeat scroll 0 0 #AAFFAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 .del {
248 248 background: none repeat scroll 0 0 #FFDDDD;
249 249
250 250 del {
251 251 background: none repeat scroll 0 0 #FFAAAA;
252 252 text-decoration: none;
253 253 }
254 254 }
255 255
256 256 /** LINE NUMBERS **/
257 257 .lineno {
258 258 padding-left: 2px !important;
259 259 padding-right: 2px;
260 260 text-align: right;
261 261 width: 32px;
262 262 -moz-user-select: none;
263 263 -webkit-user-select: none;
264 264 border-right: @border-thickness solid @grey5 !important;
265 265 border-left: 0px solid #CCC !important;
266 266 border-top: 0px solid #CCC !important;
267 267 border-bottom: none !important;
268 268
269 269 a {
270 270 &:extend(pre);
271 271 text-align: right;
272 272 padding-right: 2px;
273 273 cursor: pointer;
274 274 display: block;
275 275 width: 32px;
276 276 }
277 277 }
278 278
279 279 .context {
280 280 cursor: auto;
281 281 &:extend(pre);
282 282 }
283 283
284 284 .lineno-inline {
285 285 background: none repeat scroll 0 0 #FFF !important;
286 286 padding-left: 2px;
287 287 padding-right: 2px;
288 288 text-align: right;
289 289 width: 30px;
290 290 -moz-user-select: none;
291 291 -webkit-user-select: none;
292 292 }
293 293
294 294 /** CODE **/
295 295 .code {
296 296 display: block;
297 297 width: 100%;
298 298
299 299 td {
300 300 margin: 0;
301 301 padding: 0;
302 302 }
303 303
304 304 pre {
305 305 margin: 0;
306 306 padding: 0;
307 307 margin-left: .5em;
308 308 }
309 309 }
310 310 }
311 311
312 312
313 313 // Comments
314 314
315 315 div.comment:target {
316 316 border-left: 6px solid @comment-highlight-color !important;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
320 320
321 321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 322 //current values that might change. But to make it clear I put as a calculation
323 323 @comment-max-width: 1065px;
324 324 @pr-extra-margin: 34px;
325 325 @pr-border-spacing: 4px;
326 326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327 327
328 328 // Pull Request
329 329 .cs_files .code-difftable {
330 330 border: @border-thickness solid @grey5; //borders only on PRs
331 331
332 332 .comment-inline-form,
333 333 div.comment {
334 334 width: @pr-comment-width;
335 335 }
336 336 }
337 337
338 338 // Changeset
339 339 .code-difftable {
340 340 .comment-inline-form,
341 341 div.comment {
342 342 width: @comment-max-width;
343 343 }
344 344 }
345 345
346 346 //Style page
347 347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 348 #style-page .code-difftable{
349 349 .comment-inline-form,
350 350 div.comment {
351 351 width: @comment-max-width - @style-extra-margin;
352 352 }
353 353 }
354 354
355 355 #context-bar > h2 {
356 356 font-size: 20px;
357 357 }
358 358
359 359 #context-bar > h2> a {
360 360 font-size: 20px;
361 361 }
362 362 // end of defaults
363 363
364 364 .file_diff_buttons {
365 365 padding: 0 0 @padding;
366 366
367 367 .drop-menu {
368 368 float: left;
369 369 margin: 0 @padding 0 0;
370 370 }
371 371 .btn {
372 372 margin: 0 @padding 0 0;
373 373 }
374 374 }
375 375
376 376 .code-body.textarea.editor {
377 377 max-width: none;
378 378 padding: 15px;
379 379 }
380 380
381 381 td.injected_diff{
382 382 max-width: 1178px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385
386 386 div.diff-container,
387 387 div.diffblock{
388 388 max-width: 100%;
389 389 }
390 390
391 391 div.code-body {
392 392 max-width: 1124px;
393 393 overflow-x: auto;
394 394 overflow-y: hidden;
395 395 padding: 0;
396 396 }
397 397 div.diffblock {
398 398 border: none;
399 399 }
400 400
401 401 &.inline-form {
402 402 width: 99%
403 403 }
404 404 }
405 405
406 406
407 407 table.code-difftable {
408 408 width: 100%;
409 409 }
410 410
411 411 /** PYGMENTS COLORING **/
412 412 div.codeblock {
413 413
414 414 // TODO: johbo: Added interim to get rid of the margin around
415 415 // Select2 widgets. This needs further cleanup.
416 416 margin-top: @padding;
417 417
418 418 overflow: auto;
419 419 padding: 0px;
420 420 border: @border-thickness solid @grey5;
421 421 background: @grey6;
422 422 .border-radius(@border-radius);
423 423
424 424 #remove_gist {
425 425 float: right;
426 426 }
427 427
428 428 .author {
429 429 clear: both;
430 430 vertical-align: middle;
431 431 font-family: @text-bold;
432 432 }
433 433
434 434 .btn-mini {
435 435 float: left;
436 436 margin: 0 5px 0 0;
437 437 }
438 438
439 439 .code-header {
440 440 padding: @padding;
441 441 border-bottom: @border-thickness solid @grey5;
442 442
443 443 .rc-user {
444 444 min-width: 0;
445 445 margin-right: .5em;
446 446 }
447 447
448 448 .stats {
449 449 clear: both;
450 450 margin: 0 0 @padding 0;
451 451 padding: 0;
452 452 .left {
453 453 float: left;
454 454 clear: left;
455 455 max-width: 75%;
456 456 margin: 0 0 @padding 0;
457 457
458 458 &.item {
459 459 margin-right: @padding;
460 460 &.last { border-right: none; }
461 461 }
462 462 }
463 463 .buttons { float: right; }
464 464 .author {
465 465 height: 25px; margin-left: 15px; font-weight: bold;
466 466 }
467 467 }
468 468
469 469 .commit {
470 470 margin: 5px 0 0 26px;
471 471 font-weight: normal;
472 472 white-space: pre-wrap;
473 473 }
474 474 }
475 475
476 476 .message {
477 477 position: relative;
478 478 margin: @padding;
479 479
480 480 .codeblock-label {
481 481 margin: 0 0 1em 0;
482 482 }
483 483 }
484 484
485 485 .code-body {
486 486 padding: @padding;
487 487 background-color: #ffffff;
488 488 min-width: 100%;
489 489 box-sizing: border-box;
490 490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 491 // to have the intended size and to scroll. Should be simplified.
492 492 width: 100%;
493 493 overflow-x: auto;
494 494 }
495 495 }
496 496
497 497 .code-highlighttable,
498 498 div.codeblock {
499 499
500 500 &.readme {
501 501 background-color: white;
502 502 }
503 503
504 504 .markdown-block table {
505 505 border-collapse: collapse;
506 506
507 507 th,
508 508 td {
509 509 padding: .5em;
510 510 border: @border-thickness solid @border-default-color;
511 511 }
512 512 }
513 513
514 514 table {
515 515 border: 0px;
516 516 margin: 0;
517 517 letter-spacing: normal;
518 518
519 519
520 520 td {
521 521 border: 0px;
522 522 vertical-align: top;
523 523 }
524 524 }
525 525 }
526 526
527 527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 528 div.search-code-body {
529 529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 530 pre {
531 531 .match { background-color: #faffa6;}
532 532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 533 }
534 534 .code-highlighttable {
535 535 border-collapse: collapse;
536 536
537 537 tr:hover {
538 538 background: #fafafa;
539 539 }
540 540 td.code {
541 541 padding-left: 10px;
542 542 }
543 543 td.line {
544 544 border-right: 1px solid #ccc !important;
545 545 padding-right: 10px;
546 546 text-align: right;
547 547 font-family: "Lucida Console",Monaco,monospace;
548 548 span {
549 549 white-space: pre-wrap;
550 550 color: #666666;
551 551 }
552 552 }
553 553 }
554 554 }
555 555
556 556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 557 .code-highlight {
558 558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 560 pre div:target {background-color: @comment-highlight-color !important;}
561 561 }
562 562
563 563 .linenos a { text-decoration: none; }
564 564
565 565 .CodeMirror-selected { background: @rchighlightblue; }
566 566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 567 .CodeMirror ::selection { background: @rchighlightblue; }
568 568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569 569
570 570 .code { display: block; border:0px !important; }
571 571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 572 .codehilite {
573 573 .hll { background-color: #ffffcc }
574 574 .c { color: #408080; font-style: italic } /* Comment */
575 575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 576 .k { color: #008000; font-weight: bold } /* Keyword */
577 577 .o { color: #666666 } /* Operator */
578 578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 582 .gd { color: #A00000 } /* Generic.Deleted */
583 583 .ge { font-style: italic } /* Generic.Emph */
584 584 .gr { color: #FF0000 } /* Generic.Error */
585 585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 586 .gi { color: #00A000 } /* Generic.Inserted */
587 587 .go { color: #808080 } /* Generic.Output */
588 588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 589 .gs { font-weight: bold } /* Generic.Strong */
590 590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 591 .gt { color: #0040D0 } /* Generic.Traceback */
592 592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 595 .kp { color: #008000 } /* Keyword.Pseudo */
596 596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 597 .kt { color: #B00040 } /* Keyword.Type */
598 598 .m { color: #666666 } /* Literal.Number */
599 599 .s { color: #BA2121 } /* Literal.String */
600 600 .na { color: #7D9029 } /* Name.Attribute */
601 601 .nb { color: #008000 } /* Name.Builtin */
602 602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 603 .no { color: #880000 } /* Name.Constant */
604 604 .nd { color: #AA22FF } /* Name.Decorator */
605 605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 607 .nf { color: #0000FF } /* Name.Function */
608 608 .nl { color: #A0A000 } /* Name.Label */
609 609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 611 .nv { color: #19177C } /* Name.Variable */
612 612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 613 .w { color: #bbbbbb } /* Text.Whitespace */
614 614 .mf { color: #666666 } /* Literal.Number.Float */
615 615 .mh { color: #666666 } /* Literal.Number.Hex */
616 616 .mi { color: #666666 } /* Literal.Number.Integer */
617 617 .mo { color: #666666 } /* Literal.Number.Oct */
618 618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 619 .sc { color: #BA2121 } /* Literal.String.Char */
620 620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 625 .sx { color: #008000 } /* Literal.String.Other */
626 626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 628 .ss { color: #19177C } /* Literal.String.Symbol */
629 629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 630 .vc { color: #19177C } /* Name.Variable.Class */
631 631 .vg { color: #19177C } /* Name.Variable.Global */
632 632 .vi { color: #19177C } /* Name.Variable.Instance */
633 633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 634 }
635 635
636 636 /* customized pre blocks for markdown/rst */
637 637 pre.literal-block, .codehilite pre{
638 638 padding: @padding;
639 639 border: 1px solid @grey6;
640 640 .border-radius(@border-radius);
641 641 background-color: @grey7;
642 642 }
643 643
644 644
645 645 /* START NEW CODE BLOCK CSS */
646 646
647 647 @cb-line-height: 18px;
648 648 @cb-line-code-padding: 10px;
649 649 @cb-text-padding: 5px;
650 650
651 651 @pill-padding: 2px 7px;
652 652
653 653 input.filediff-collapse-state {
654 654 display: none;
655 655
656 656 &:checked + .filediff { /* file diff is collapsed */
657 657 .cb {
658 658 display: none
659 659 }
660 660 .filediff-collapse-indicator {
661 661 width: 0;
662 662 height: 0;
663 663 border-style: solid;
664 664 border-width: 6.5px 0 6.5px 11.3px;
665 665 border-color: transparent transparent transparent #ccc;
666 666 }
667 667 .filediff-menu {
668 668 display: none;
669 669 }
670 670 margin: 10px 0 0 0;
671 671 }
672 672
673 673 &+ .filediff { /* file diff is expanded */
674 674 .filediff-collapse-indicator {
675 675 width: 0;
676 676 height: 0;
677 677 border-style: solid;
678 678 border-width: 11.3px 6.5px 0 6.5px;
679 679 border-color: #ccc transparent transparent transparent;
680 680 }
681 681 .filediff-menu {
682 682 display: block;
683 683 }
684 684 margin: 10px 0;
685 685 &:nth-child(2) {
686 686 margin: 0;
687 687 }
688 688 }
689 689 }
690 690 .cs_files {
691 691 clear: both;
692 692 }
693 693
694 694 .diffset-menu {
695 695 margin-bottom: 20px;
696 696 }
697 697 .diffset {
698 698 margin: 20px auto;
699 699 .diffset-heading {
700 700 border: 1px solid @grey5;
701 701 margin-bottom: -1px;
702 702 // margin-top: 20px;
703 703 h2 {
704 704 margin: 0;
705 705 line-height: 38px;
706 706 padding-left: 10px;
707 707 }
708 708 .btn {
709 709 margin: 0;
710 710 }
711 711 background: @grey6;
712 712 display: block;
713 713 padding: 5px;
714 714 }
715 715 .diffset-heading-warning {
716 716 background: @alert3-inner;
717 717 border: 1px solid @alert3;
718 718 }
719 719 &.diffset-comments-disabled {
720 720 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
721 721 display: none !important;
722 722 }
723 723 }
724 724 }
725 725
726 726 .pill {
727 727 display: block;
728 728 float: left;
729 729 padding: @pill-padding;
730 730 }
731 731 .pill-group {
732 732 .pill {
733 733 opacity: .8;
734 734 &:first-child {
735 735 border-radius: @border-radius 0 0 @border-radius;
736 736 }
737 737 &:last-child {
738 738 border-radius: 0 @border-radius @border-radius 0;
739 739 }
740 740 &:only-child {
741 741 border-radius: @border-radius;
742 742 }
743 743 }
744 744 }
745 745
746 746 /* Main comments*/
747 747 #comments {
748 748 .comment-selected {
749 749 border-left: 6px solid @comment-highlight-color;
750 750 padding-left: 3px;
751 751 margin-left: -9px;
752 752 }
753 753 }
754 754
755 755 .filediff {
756 756 border: 1px solid @grey5;
757 757
758 758 /* START OVERRIDES */
759 759 .code-highlight {
760 760 border: none; // TODO: remove this border from the global
761 761 // .code-highlight, it doesn't belong there
762 762 }
763 763 label {
764 764 margin: 0; // TODO: remove this margin definition from global label
765 765 // it doesn't belong there - if margin on labels
766 766 // are needed for a form they should be defined
767 767 // in the form's class
768 768 }
769 769 /* END OVERRIDES */
770 770
771 771 * {
772 772 box-sizing: border-box;
773 773 }
774 774 .filediff-anchor {
775 775 visibility: hidden;
776 776 }
777 777 &:hover {
778 778 .filediff-anchor {
779 779 visibility: visible;
780 780 }
781 781 }
782 782
783 783 .filediff-collapse-indicator {
784 784 border-style: solid;
785 785 float: left;
786 786 margin: 4px 0px 0 0;
787 787 cursor: pointer;
788 788 }
789 789
790 790 .filediff-heading {
791 791 background: @grey7;
792 792 cursor: pointer;
793 793 display: block;
794 794 padding: 5px 10px;
795 795 }
796 796 .filediff-heading:after {
797 797 content: "";
798 798 display: table;
799 799 clear: both;
800 800 }
801 801 .filediff-heading:hover {
802 802 background: #e1e9f4 !important;
803 803 }
804 804
805 805 .filediff-menu {
806 806 float: right;
807 807 text-align: right;
808 808 padding: 5px 5px 5px 0px;
809 809
810 810 &> a,
811 811 &> span {
812 812 padding: 1px;
813 813 }
814 814 }
815 815
816 816 .pill {
817 817 &[op="name"] {
818 818 background: none;
819 819 color: @grey2;
820 820 opacity: 1;
821 821 color: white;
822 822 }
823 823 &[op="limited"] {
824 824 background: @grey2;
825 825 color: white;
826 826 }
827 827 &[op="binary"] {
828 828 background: @color7;
829 829 color: white;
830 830 }
831 831 &[op="modified"] {
832 832 background: @alert1;
833 833 color: white;
834 834 }
835 835 &[op="renamed"] {
836 836 background: @color4;
837 837 color: white;
838 838 }
839 839 &[op="mode"] {
840 840 background: @grey3;
841 841 color: white;
842 842 }
843 843 &[op="symlink"] {
844 844 background: @color8;
845 845 color: white;
846 846 }
847 847
848 848 &[op="added"] { /* added lines */
849 849 background: @alert1;
850 850 color: white;
851 851 }
852 852 &[op="deleted"] { /* deleted lines */
853 853 background: @alert2;
854 854 color: white;
855 855 }
856 856
857 857 &[op="created"] { /* created file */
858 858 background: @alert1;
859 859 color: white;
860 860 }
861 861 &[op="removed"] { /* deleted file */
862 862 background: @color5;
863 863 color: white;
864 864 }
865 865 }
866 866
867 867 .filediff-collapse-button, .filediff-expand-button {
868 868 cursor: pointer;
869 869 }
870 870 .filediff-collapse-button {
871 871 display: inline;
872 872 }
873 873 .filediff-expand-button {
874 874 display: none;
875 875 }
876 876 .filediff-collapsed .filediff-collapse-button {
877 877 display: none;
878 878 }
879 879 .filediff-collapsed .filediff-expand-button {
880 880 display: inline;
881 881 }
882 882
883 883 /**** COMMENTS ****/
884 884
885 885 .filediff-menu {
886 886 .show-comment-button {
887 887 display: none;
888 888 }
889 889 }
890 890 &.hide-comments {
891 891 .inline-comments {
892 892 display: none;
893 893 }
894 894 .filediff-menu {
895 895 .show-comment-button {
896 896 display: inline;
897 897 }
898 898 .hide-comment-button {
899 899 display: none;
900 900 }
901 901 }
902 902 }
903 903
904 904 .hide-line-comments {
905 905 .inline-comments {
906 906 display: none;
907 907 }
908 908 }
909 909
910 910 /**** END COMMENTS ****/
911 911
912 912 }
913 913
914 914 .filediff-outdated {
915 915 padding: 8px 0;
916 916
917 917 .filediff-heading {
918 918 opacity: .5;
919 919 }
920 920 }
921 921
922 922 table.cb {
923 923 width: 100%;
924 924 border-collapse: collapse;
925 925
926 926 .cb-text {
927 927 padding: @cb-text-padding;
928 928 }
929 929 .cb-hunk {
930 930 padding: @cb-text-padding;
931 931 }
932 932 .cb-expand {
933 933 display: none;
934 934 }
935 935 .cb-collapse {
936 936 display: inline;
937 937 }
938 938 &.cb-collapsed {
939 939 .cb-line {
940 940 display: none;
941 941 }
942 942 .cb-expand {
943 943 display: inline;
944 944 }
945 945 .cb-collapse {
946 946 display: none;
947 947 }
948 948 }
949 949
950 950 /* intentionally general selector since .cb-line-selected must override it
951 951 and they both use !important since the td itself may have a random color
952 952 generated by annotation blocks. TLDR: if you change it, make sure
953 953 annotated block selection and line selection in file view still work */
954 954 .cb-line-fresh .cb-content {
955 955 background: white !important;
956 956 }
957 957 .cb-warning {
958 958 background: #fff4dd;
959 959 }
960 960
961 961 &.cb-diff-sideside {
962 962 td {
963 963 &.cb-content {
964 964 width: 50%;
965 965 }
966 966 }
967 967 }
968 968
969 969 tr {
970 970 &.cb-annotate {
971 971 border-top: 1px solid #eee;
972 972 }
973 973
974 974 &.cb-hunk {
975 975 font-family: @font-family-monospace;
976 976 color: rgba(0, 0, 0, 0.3);
977 977
978 978 td {
979 979 &:first-child {
980 980 background: #edf2f9;
981 981 }
982 982 &:last-child {
983 983 background: #f4f7fb;
984 984 }
985 985 }
986 986 }
987 987 }
988 988
989 989
990 990 td {
991 991 vertical-align: top;
992 992 padding: 0;
993 993
994 994 &.cb-content {
995 995 font-size: 12.35px;
996 996
997 997 &.cb-line-selected .cb-code {
998 998 background: @comment-highlight-color !important;
999 999 }
1000 1000
1001 1001 span.cb-code {
1002 1002 line-height: @cb-line-height;
1003 1003 padding-left: @cb-line-code-padding;
1004 1004 padding-right: @cb-line-code-padding;
1005 1005 display: block;
1006 1006 white-space: pre-wrap;
1007 1007 font-family: @font-family-monospace;
1008 1008 word-break: break-all;
1009 1009 .nonl {
1010 1010 color: @color5;
1011 1011 }
1012 1012 }
1013 1013
1014 1014 &> button.cb-comment-box-opener {
1015 1015
1016 1016 padding: 2px 2px 1px 3px;
1017 1017 margin-left: -6px;
1018 1018 margin-top: -1px;
1019 1019
1020 1020 border-radius: @border-radius;
1021 1021 position: absolute;
1022 1022 display: none;
1023 1023 }
1024 1024 .cb-comment {
1025 1025 margin-top: 10px;
1026 1026 white-space: normal;
1027 1027 }
1028 1028 }
1029 1029 &:hover {
1030 1030 button.cb-comment-box-opener {
1031 1031 display: block;
1032 1032 }
1033 1033 &+ td button.cb-comment-box-opener {
1034 1034 display: block
1035 1035 }
1036 1036 }
1037 1037
1038 1038 &.cb-data {
1039 1039 text-align: right;
1040 1040 width: 30px;
1041 1041 font-family: @font-family-monospace;
1042 1042
1043 1043 .icon-comment {
1044 1044 cursor: pointer;
1045 1045 }
1046 1046 &.cb-line-selected > div {
1047 1047 display: block;
1048 1048 background: @comment-highlight-color !important;
1049 1049 line-height: @cb-line-height;
1050 1050 color: rgba(0, 0, 0, 0.3);
1051 1051 }
1052 1052 }
1053 1053
1054 1054 &.cb-lineno {
1055 1055 padding: 0;
1056 1056 width: 50px;
1057 1057 color: rgba(0, 0, 0, 0.3);
1058 1058 text-align: right;
1059 1059 border-right: 1px solid #eee;
1060 1060 font-family: @font-family-monospace;
1061 1061
1062 1062 a::before {
1063 1063 content: attr(data-line-no);
1064 1064 }
1065 1065 &.cb-line-selected a {
1066 1066 background: @comment-highlight-color !important;
1067 1067 }
1068 1068
1069 1069 a {
1070 1070 display: block;
1071 1071 padding-right: @cb-line-code-padding;
1072 1072 padding-left: @cb-line-code-padding;
1073 1073 line-height: @cb-line-height;
1074 1074 color: rgba(0, 0, 0, 0.3);
1075 1075 }
1076 1076 }
1077 1077
1078 1078 &.cb-empty {
1079 1079 background: @grey7;
1080 1080 }
1081 1081
1082 1082 ins {
1083 1083 color: black;
1084 1084 background: #a6f3a6;
1085 1085 text-decoration: none;
1086 1086 }
1087 1087 del {
1088 1088 color: black;
1089 1089 background: #f8cbcb;
1090 1090 text-decoration: none;
1091 1091 }
1092 1092 &.cb-addition {
1093 1093 background: #ecffec;
1094 1094
1095 1095 &.blob-lineno {
1096 1096 background: #ddffdd;
1097 1097 }
1098 1098 }
1099 1099 &.cb-deletion {
1100 1100 background: #ffecec;
1101 1101
1102 1102 &.blob-lineno {
1103 1103 background: #ffdddd;
1104 1104 }
1105 1105 }
1106 1106 &.cb-annotate-message-spacer {
1107 1107 width:8px;
1108 padding: 1px 0px 0px 3px;
1108 1109 }
1109 1110 &.cb-annotate-info {
1110 1111 width: 320px;
1111 1112 min-width: 320px;
1112 1113 max-width: 320px;
1113 1114 padding: 5px 2px;
1114 1115 font-size: 13px;
1115 1116
1116 1117 .cb-annotate-message {
1117 1118 padding: 2px 0px 0px 0px;
1118 1119 white-space: pre-line;
1119 1120 overflow: hidden;
1120 1121 }
1121 1122 .rc-user {
1122 1123 float: none;
1123 1124 padding: 0 6px 0 17px;
1124 1125 min-width: unset;
1125 1126 min-height: unset;
1126 1127 }
1127 1128 }
1128 1129
1129 1130 &.cb-annotate-revision {
1130 1131 cursor: pointer;
1131 1132 text-align: right;
1132 1133 padding: 1px 3px 0px 3px;
1133 1134 }
1134 1135 }
1135 1136 }
@@ -1,57 +1,58 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('home', '/', []);
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
18 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
19 19 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
20 20 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
21 21 pyroutes.register('gists', '/_admin/gists', []);
22 22 pyroutes.register('new_gist', '/_admin/gists/new', []);
23 23 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
24 24 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
25 25 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
26 26 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
27 27 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
28 28 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
29 29 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
30 30 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
31 31 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
32 32 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
33 33 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
34 34 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
35 35 pyroutes.register('compare_url', '/%(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']);
36 36 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
37 37 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
38 38 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
39 39 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
40 40 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
41 41 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
42 42 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
43 43 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
44 44 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
45 45 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
46 46 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
47 47 pyroutes.register('changelog_elements', '/%(repo_name)s/changelog_details', ['repo_name']);
48 48 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
49 49 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
50 50 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
51 51 pyroutes.register('files_annotate_home', '/%(repo_name)s/annotate/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
52 pyroutes.register('files_annotate_previous', '/%(repo_name)s/annotate-previous/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
52 53 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
53 54 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
54 55 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
55 56 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
56 57 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
57 58 }
@@ -1,71 +1,90 b''
1 1 <%def name="render_line(line_num, tokens,
2 2 annotation=None,
3 3 bgcolor=None, show_annotation=None)">
4 4 <%
5 5 from rhodecode.lib.codeblocks import render_tokenstream
6 6 # avoid module lookup for performance
7 7 html_escape = h.html_escape
8 8 %>
9 9 <tr class="cb-line cb-line-fresh ${'cb-annotate' if show_annotation else ''}"
10 10 %if annotation:
11 11 data-revision="${annotation.revision}"
12 12 %endif
13 13 >
14 14
15 15 % if annotation:
16 16 % if show_annotation:
17 17 <td class="cb-annotate-info tooltip"
18 18 title="Author: ${annotation.author | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}"
19 19 >
20 20 ${h.gravatar_with_user(annotation.author, 16) | n}
21 21 <div class="cb-annotate-message truncate-wrap">${h.chop_at_smart(annotation.message, '\n', suffix_if_chopped='...')}</div>
22 22 </td>
23 <td class="cb-annotate-message-spacer"></td>
23 <td class="cb-annotate-message-spacer">
24 <a class="tooltip" href="#show-previous-annotation" onclick="return annotationController.previousAnnotation('${annotation.raw_id}', '${c.f_path}')" title="${_('view annotation from before this change')}">
25 <i class="icon-left"></i>
26 </a>
27 </td>
24 28 <td
25 29 class="cb-annotate-revision"
26 30 data-revision="${annotation.revision}"
27 31 onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
28 32 style="background: ${bgcolor}">
29 33 <a class="cb-annotate" href="${h.url('changeset_home',repo_name=c.repo_name,revision=annotation.raw_id)}">
30 34 r${annotation.revision}
31 35 </a>
32 36 </td>
33 37 % else:
34 38 <td></td>
35 39 <td class="cb-annotate-message-spacer"></td>
36 40 <td
37 41 class="cb-annotate-revision"
38 42 data-revision="${annotation.revision}"
39 43 onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
40 44 style="background: ${bgcolor}">
41 45 </td>
42 46 % endif
43 47 % else:
44 48 <td colspan="3"></td>
45 49 % endif
46 50
47 51
48 52 <td class="cb-lineno" id="L${line_num}">
49 53 <a data-line-no="${line_num}" href="#L${line_num}"></a>
50 54 </td>
51 55 <td class="cb-content cb-content-fresh"
52 56 %if bgcolor:
53 57 style="background: ${bgcolor}"
54 58 %endif
55 59 >
56 60 ## newline at end is necessary for highlight to work when line is empty
57 61 ## and for copy pasting code to work as expected
58 62 <span class="cb-code">${render_tokenstream(tokens)|n}${'\n'}</span>
59 63 </td>
60 64 </tr>
61 65 </%def>
62 66
63 67 <%def name="render_annotation_lines(annotation, lines, color_hasher)">
64 68 % for line_num, tokens in lines:
65 69 ${render_line(line_num, tokens,
66 70 bgcolor=color_hasher(annotation and annotation.raw_id or ''),
67 71 annotation=annotation, show_annotation=loop.first
68 72 )}
69 73 % endfor
74 <script>
75 var AnnotationController = function() {
76 var self = this;
70 77
78 this.previousAnnotation = function(commitId, fPath) {
79 var params = {
80 'repo_name': templateContext.repo_name,
81 'revision': commitId,
82 'f_path': fPath
83 };
84 window.location = pyroutes.url('files_annotate_previous', params);
85 return false;
86 };
87 };
88 var annotationController = new AnnotationController();
89 </script>
71 90 </%def>
General Comments 0
You need to be logged in to leave comments. Login now