##// END OF EJS Templates
repo-commits: ported changeset code into pyramid views....
marcink -
r1951:965019b0 default
parent child Browse files
Show More

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

This diff has been collapsed as it changes many lines, (557 lines changed) Show them Hide them
@@ -0,0 +1,557 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import logging
23 import collections
24
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 from pyramid.view import view_config
27 from pyramid.renderers import render
28 from pyramid.response import Response
29
30 from rhodecode.apps._base import RepoAppView
31
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 import rhodecode.lib.helpers as h
39 from rhodecode.lib.utils2 import safe_unicode, safe_int
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 from rhodecode.lib.vcs.exceptions import (
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.meta import Session
47
48
49 log = logging.getLogger(__name__)
50
51
52 def _update_with_GET(params, request):
53 for k in ['diff1', 'diff2', 'diff']:
54 params[k] += request.GET.getall(k)
55
56
57 def get_ignore_ws(fid, request):
58 ig_ws_global = request.GET.get('ignorews')
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 if ig_ws:
61 try:
62 return int(ig_ws[0].split(':')[-1])
63 except Exception:
64 pass
65 return ig_ws_global
66
67
68 def _ignorews_url(request, fileid=None):
69 _ = request.translate
70 fileid = str(fileid) if fileid else None
71 params = collections.defaultdict(list)
72 _update_with_GET(params, request)
73 label = _('Show whitespace')
74 tooltiplbl = _('Show whitespace for all diffs')
75 ig_ws = get_ignore_ws(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
77
78 if ig_ws is None:
79 params['ignorews'] += [1]
80 label = _('Ignore whitespace')
81 tooltiplbl = _('Ignore whitespace for all diffs')
82 ctx_key = 'context'
83 ctx_val = ln_ctx
84
85 # if we have passed in ln_ctx pass it along to our params
86 if ln_ctx:
87 params[ctx_key] += [ctx_val]
88
89 if fileid:
90 params['anchor'] = 'a_' + fileid
91 return h.link_to(label, request.current_route_path(_query=params),
92 title=tooltiplbl, class_='tooltip')
93
94
95 def get_line_ctx(fid, request):
96 ln_ctx_global = request.GET.get('context')
97 if fid:
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 else:
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 if ln_ctx:
103 ln_ctx = [ln_ctx]
104
105 if ln_ctx:
106 retval = ln_ctx[0].split(':')[-1]
107 else:
108 retval = ln_ctx_global
109
110 try:
111 return int(retval)
112 except Exception:
113 return 3
114
115
116 def _context_url(request, fileid=None):
117 """
118 Generates a url for context lines.
119
120 :param fileid:
121 """
122
123 _ = request.translate
124 fileid = str(fileid) if fileid else None
125 ig_ws = get_ignore_ws(fileid, request)
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127
128 params = collections.defaultdict(list)
129 _update_with_GET(params, request)
130
131 if ln_ctx > 0:
132 params['context'] += [ln_ctx]
133
134 if ig_ws:
135 ig_ws_key = 'ignorews'
136 ig_ws_val = 1
137 params[ig_ws_key] += [ig_ws_val]
138
139 lbl = _('Increase context')
140 tooltiplbl = _('Increase context for all diffs')
141
142 if fileid:
143 params['anchor'] = 'a_' + fileid
144 return h.link_to(lbl, request.current_route_path(_query=params),
145 title=tooltiplbl, class_='tooltip')
146
147
148 class RepoCommitsView(RepoAppView):
149 def load_default_context(self):
150 c = self._get_local_tmpl_context(include_app_defaults=True)
151
152 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
153 c.repo_info = self.db_repo
154 c.rhodecode_repo = self.rhodecode_vcs_repo
155
156 self._register_global_c(c)
157 return c
158
159 def _commit(self, commit_id_range, method):
160 _ = self.request.translate
161 c = self.load_default_context()
162 c.ignorews_url = _ignorews_url
163 c.context_url = _context_url
164 c.fulldiff = self.request.GET.get('fulldiff')
165
166 # fetch global flags of ignore ws or context lines
167 context_lcl = get_line_ctx('', self.request)
168 ign_whitespace_lcl = get_ignore_ws('', self.request)
169
170 # diff_limit will cut off the whole diff if the limit is applied
171 # otherwise it will just hide the big files from the front-end
172 diff_limit = c.visual.cut_off_limit_diff
173 file_limit = c.visual.cut_off_limit_file
174
175 # get ranges of commit ids if preset
176 commit_range = commit_id_range.split('...')[:2]
177
178 try:
179 pre_load = ['affected_files', 'author', 'branch', 'date',
180 'message', 'parents']
181
182 if len(commit_range) == 2:
183 commits = self.rhodecode_vcs_repo.get_commits(
184 start_id=commit_range[0], end_id=commit_range[1],
185 pre_load=pre_load)
186 commits = list(commits)
187 else:
188 commits = [self.rhodecode_vcs_repo.get_commit(
189 commit_id=commit_id_range, pre_load=pre_load)]
190
191 c.commit_ranges = commits
192 if not c.commit_ranges:
193 raise RepositoryError(
194 'The commit range returned an empty result')
195 except CommitDoesNotExistError:
196 msg = _('No such commit exists for this repository')
197 h.flash(msg, category='error')
198 raise HTTPNotFound()
199 except Exception:
200 log.exception("General failure")
201 raise HTTPNotFound()
202
203 c.changes = OrderedDict()
204 c.lines_added = 0
205 c.lines_deleted = 0
206
207 # auto collapse if we have more than limit
208 collapse_limit = diffs.DiffProcessor._collapse_commits_over
209 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
210
211 c.commit_statuses = ChangesetStatus.STATUSES
212 c.inline_comments = []
213 c.files = []
214
215 c.statuses = []
216 c.comments = []
217 c.unresolved_comments = []
218 if len(c.commit_ranges) == 1:
219 commit = c.commit_ranges[0]
220 c.comments = CommentsModel().get_comments(
221 self.db_repo.repo_id,
222 revision=commit.raw_id)
223 c.statuses.append(ChangesetStatusModel().get_status(
224 self.db_repo.repo_id, commit.raw_id))
225 # comments from PR
226 statuses = ChangesetStatusModel().get_statuses(
227 self.db_repo.repo_id, commit.raw_id,
228 with_revisions=True)
229 prs = set(st.pull_request for st in statuses
230 if st.pull_request is not None)
231 # from associated statuses, check the pull requests, and
232 # show comments from them
233 for pr in prs:
234 c.comments.extend(pr.comments)
235
236 c.unresolved_comments = CommentsModel()\
237 .get_commit_unresolved_todos(commit.raw_id)
238
239 diff = None
240 # Iterate over ranges (default commit view is always one commit)
241 for commit in c.commit_ranges:
242 c.changes[commit.raw_id] = []
243
244 commit2 = commit
245 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
246
247 _diff = self.rhodecode_vcs_repo.get_diff(
248 commit1, commit2,
249 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
250 diff_processor = diffs.DiffProcessor(
251 _diff, format='newdiff', diff_limit=diff_limit,
252 file_limit=file_limit, show_full_diff=c.fulldiff)
253
254 commit_changes = OrderedDict()
255 if method == 'show':
256 _parsed = diff_processor.prepare()
257 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
258
259 _parsed = diff_processor.prepare()
260
261 def _node_getter(commit):
262 def get_node(fname):
263 try:
264 return commit.get_node(fname)
265 except NodeDoesNotExistError:
266 return None
267 return get_node
268
269 inline_comments = CommentsModel().get_inline_comments(
270 self.db_repo.repo_id, revision=commit.raw_id)
271 c.inline_cnt = CommentsModel().get_inline_comments_count(
272 inline_comments)
273
274 diffset = codeblocks.DiffSet(
275 repo_name=self.db_repo_name,
276 source_node_getter=_node_getter(commit1),
277 target_node_getter=_node_getter(commit2),
278 comments=inline_comments)
279 diffset = diffset.render_patchset(
280 _parsed, commit1.raw_id, commit2.raw_id)
281
282 c.changes[commit.raw_id] = diffset
283 else:
284 # downloads/raw we only need RAW diff nothing else
285 diff = diff_processor.as_raw()
286 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
287
288 # sort comments by how they were generated
289 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
290
291 if len(c.commit_ranges) == 1:
292 c.commit = c.commit_ranges[0]
293 c.parent_tmpl = ''.join(
294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
295
296 if method == 'download':
297 response = Response(diff)
298 response.content_type = 'text/plain'
299 response.content_disposition = (
300 'attachment; filename=%s.diff' % commit_id_range[:12])
301 return response
302 elif method == 'patch':
303 c.diff = safe_unicode(diff)
304 patch = render(
305 'rhodecode:templates/changeset/patch_changeset.mako',
306 self._get_template_context(c), self.request)
307 response = Response(patch)
308 response.content_type = 'text/plain'
309 return response
310 elif method == 'raw':
311 response = Response(diff)
312 response.content_type = 'text/plain'
313 return response
314 elif method == 'show':
315 if len(c.commit_ranges) == 1:
316 html = render(
317 'rhodecode:templates/changeset/changeset.mako',
318 self._get_template_context(c), self.request)
319 return Response(html)
320 else:
321 c.ancestor = None
322 c.target_repo = self.db_repo
323 html = render(
324 'rhodecode:templates/changeset/changeset_range.mako',
325 self._get_template_context(c), self.request)
326 return Response(html)
327
328 raise HTTPBadRequest()
329
330 @LoginRequired()
331 @HasRepoPermissionAnyDecorator(
332 'repository.read', 'repository.write', 'repository.admin')
333 @view_config(
334 route_name='repo_commit', request_method='GET',
335 renderer=None)
336 def repo_commit_show(self):
337 commit_id = self.request.matchdict['commit_id']
338 return self._commit(commit_id, method='show')
339
340 @LoginRequired()
341 @HasRepoPermissionAnyDecorator(
342 'repository.read', 'repository.write', 'repository.admin')
343 @view_config(
344 route_name='repo_commit_raw', request_method='GET',
345 renderer=None)
346 @view_config(
347 route_name='repo_commit_raw_deprecated', request_method='GET',
348 renderer=None)
349 def repo_commit_raw(self):
350 commit_id = self.request.matchdict['commit_id']
351 return self._commit(commit_id, method='raw')
352
353 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
356 @view_config(
357 route_name='repo_commit_patch', request_method='GET',
358 renderer=None)
359 def repo_commit_patch(self):
360 commit_id = self.request.matchdict['commit_id']
361 return self._commit(commit_id, method='patch')
362
363 @LoginRequired()
364 @HasRepoPermissionAnyDecorator(
365 'repository.read', 'repository.write', 'repository.admin')
366 @view_config(
367 route_name='repo_commit_download', request_method='GET',
368 renderer=None)
369 def repo_commit_download(self):
370 commit_id = self.request.matchdict['commit_id']
371 return self._commit(commit_id, method='download')
372
373 @LoginRequired()
374 @NotAnonymous()
375 @HasRepoPermissionAnyDecorator(
376 'repository.read', 'repository.write', 'repository.admin')
377 @CSRFRequired()
378 @view_config(
379 route_name='repo_commit_comment_create', request_method='POST',
380 renderer='json_ext')
381 def repo_commit_comment_create(self):
382 _ = self.request.translate
383 commit_id = self.request.matchdict['commit_id']
384
385 c = self.load_default_context()
386 status = self.request.POST.get('changeset_status', None)
387 text = self.request.POST.get('text')
388 comment_type = self.request.POST.get('comment_type')
389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
390
391 if status:
392 text = text or (_('Status change %(transition_icon)s %(status)s')
393 % {'transition_icon': '>',
394 'status': ChangesetStatus.get_status_lbl(status)})
395
396 multi_commit_ids = []
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 if _commit_id not in multi_commit_ids:
400 multi_commit_ids.append(_commit_id)
401
402 commit_ids = multi_commit_ids or [commit_id]
403
404 comment = None
405 for current_id in filter(None, commit_ids):
406 comment = CommentsModel().create(
407 text=text,
408 repo=self.db_repo.repo_id,
409 user=self._rhodecode_db_user.user_id,
410 commit_id=current_id,
411 f_path=self.request.POST.get('f_path'),
412 line_no=self.request.POST.get('line'),
413 status_change=(ChangesetStatus.get_status_lbl(status)
414 if status else None),
415 status_change_type=status,
416 comment_type=comment_type,
417 resolves_comment_id=resolves_comment_id
418 )
419
420 # get status if set !
421 if status:
422 # if latest status was from pull request and it's closed
423 # disallow changing status !
424 # dont_allow_on_closed_pull_request = True !
425
426 try:
427 ChangesetStatusModel().set_status(
428 self.db_repo.repo_id,
429 status,
430 self._rhodecode_db_user.user_id,
431 comment,
432 revision=current_id,
433 dont_allow_on_closed_pull_request=True
434 )
435 except StatusChangeOnClosedPullRequestError:
436 msg = _('Changing the status of a commit associated with '
437 'a closed pull request is not allowed')
438 log.exception(msg)
439 h.flash(msg, category='warning')
440 raise HTTPFound(h.route_path(
441 'repo_commit', repo_name=self.db_repo_name,
442 commit_id=current_id))
443
444 # finalize, commit and redirect
445 Session().commit()
446
447 data = {
448 'target_id': h.safeid(h.safe_unicode(
449 self.request.POST.get('f_path'))),
450 }
451 if comment:
452 c.co = comment
453 rendered_comment = render(
454 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 self._get_template_context(c), self.request)
456
457 data.update(comment.get_dict())
458 data.update({'rendered_text': rendered_comment})
459
460 return data
461
462 @LoginRequired()
463 @NotAnonymous()
464 @HasRepoPermissionAnyDecorator(
465 'repository.read', 'repository.write', 'repository.admin')
466 @CSRFRequired()
467 @view_config(
468 route_name='repo_commit_comment_preview', request_method='POST',
469 renderer='string', xhr=True)
470 def repo_commit_comment_preview(self):
471 # Technically a CSRF token is not needed as no state changes with this
472 # call. However, as this is a POST is better to have it, so automated
473 # tools don't flag it as potential CSRF.
474 # Post is required because the payload could be bigger than the maximum
475 # allowed by GET.
476
477 text = self.request.POST.get('text')
478 renderer = self.request.POST.get('renderer') or 'rst'
479 if text:
480 return h.render(text, renderer=renderer, mentions=True)
481 return ''
482
483 @LoginRequired()
484 @NotAnonymous()
485 @HasRepoPermissionAnyDecorator(
486 'repository.read', 'repository.write', 'repository.admin')
487 @CSRFRequired()
488 @view_config(
489 route_name='repo_commit_comment_delete', request_method='POST',
490 renderer='json_ext')
491 def repo_commit_comment_delete(self):
492 commit_id = self.request.matchdict['commit_id']
493 comment_id = self.request.matchdict['comment_id']
494
495 comment = ChangesetComment.get_or_404(safe_int(comment_id))
496 if not comment:
497 log.debug('Comment with id:%s not found, skipping', comment_id)
498 # comment already deleted in another call probably
499 return True
500
501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
502 super_admin = h.HasPermissionAny('hg.admin')()
503 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
505 comment_repo_admin = is_repo_admin and is_repo_comment
506
507 if super_admin or comment_owner or comment_repo_admin:
508 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
509 Session().commit()
510 return True
511 else:
512 log.warning('No permissions for user %s to delete comment_id: %s',
513 self._rhodecode_db_user, comment_id)
514 raise HTTPNotFound()
515
516 @LoginRequired()
517 @HasRepoPermissionAnyDecorator(
518 'repository.read', 'repository.write', 'repository.admin')
519 @view_config(
520 route_name='repo_commit_data', request_method='GET',
521 renderer='json_ext', xhr=True)
522 def repo_commit_data(self):
523 commit_id = self.request.matchdict['commit_id']
524 self.load_default_context()
525
526 try:
527 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
528 except CommitDoesNotExistError as e:
529 return EmptyCommit(message=str(e))
530
531 @LoginRequired()
532 @HasRepoPermissionAnyDecorator(
533 'repository.read', 'repository.write', 'repository.admin')
534 @view_config(
535 route_name='repo_commit_children', request_method='GET',
536 renderer='json_ext', xhr=True)
537 def repo_commit_children(self):
538 commit_id = self.request.matchdict['commit_id']
539 self.load_default_context()
540
541 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
542 result = {"results": commit.children}
543 return result
544
545 @LoginRequired()
546 @HasRepoPermissionAnyDecorator(
547 'repository.read', 'repository.write', 'repository.admin')
548 @view_config(
549 route_name='repo_commit_parents', request_method='GET',
550 renderer='json_ext')
551 def repo_commit_parents(self):
552 commit_id = self.request.matchdict['commit_id']
553 self.load_default_context()
554
555 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
556 result = {"results": commit.parents}
557 return result
@@ -1,677 +1,678 b''
1 1
2 2
3 3 ################################################################################
4 4 ## RHODECODE COMMUNITY EDITION CONFIGURATION ##
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10
11 11 ################################################################################
12 12 ## EMAIL CONFIGURATION ##
13 13 ## Uncomment and replace with the email address which should receive ##
14 14 ## any error reports after an application crash ##
15 15 ## Additionally these settings will be used by the RhodeCode mailing system ##
16 16 ################################################################################
17 17
18 18 ## prefix all emails subjects with given prefix, helps filtering out emails
19 19 #email_prefix = [RhodeCode]
20 20
21 21 ## email FROM address all mails will be sent
22 22 #app_email_from = rhodecode-noreply@localhost
23 23
24 24 ## Uncomment and replace with the address which should receive any error report
25 25 ## note: using appenlight for error handling doesn't need this to be uncommented
26 26 #email_to = admin@localhost
27 27
28 28 ## in case of Application errors, sent an error email form
29 29 #error_email_from = rhodecode_error@localhost
30 30
31 31 ## additional error message to be send in case of server crash
32 32 #error_message =
33 33
34 34
35 35 #smtp_server = mail.server.com
36 36 #smtp_username =
37 37 #smtp_password =
38 38 #smtp_port =
39 39 #smtp_use_tls = false
40 40 #smtp_use_ssl = true
41 41 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
42 42 #smtp_auth =
43 43
44 44 [server:main]
45 45 ## COMMON ##
46 46 host = 127.0.0.1
47 47 port = 5000
48 48
49 49 ##################################
50 50 ## WAITRESS WSGI SERVER ##
51 51 ## Recommended for Development ##
52 52 ##################################
53 53
54 54 use = egg:waitress#main
55 55 ## number of worker threads
56 56 threads = 5
57 57 ## MAX BODY SIZE 100GB
58 58 max_request_body_size = 107374182400
59 59 ## Use poll instead of select, fixes file descriptors limits problems.
60 60 ## May not work on old windows systems.
61 61 asyncore_use_poll = true
62 62
63 63
64 64 ##########################
65 65 ## GUNICORN WSGI SERVER ##
66 66 ##########################
67 67 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
68 68
69 69 #use = egg:gunicorn#main
70 70 ## Sets the number of process workers. You must set `instance_id = *`
71 71 ## when this option is set to more than one worker, recommended
72 72 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
73 73 ## The `instance_id = *` must be set in the [app:main] section below
74 74 #workers = 2
75 75 ## number of threads for each of the worker, must be set to 1 for gevent
76 76 ## generally recommened to be at 1
77 77 #threads = 1
78 78 ## process name
79 79 #proc_name = rhodecode
80 80 ## type of worker class, one of sync, gevent
81 81 ## recommended for bigger setup is using of of other than sync one
82 82 #worker_class = sync
83 83 ## The maximum number of simultaneous clients. Valid only for Gevent
84 84 #worker_connections = 10
85 85 ## max number of requests that worker will handle before being gracefully
86 86 ## restarted, could prevent memory leaks
87 87 #max_requests = 1000
88 88 #max_requests_jitter = 30
89 89 ## amount of time a worker can spend with handling a request before it
90 90 ## gets killed and restarted. Set to 6hrs
91 91 #timeout = 21600
92 92
93 93
94 94 ## prefix middleware for RhodeCode.
95 95 ## recommended when using proxy setup.
96 96 ## allows to set RhodeCode under a prefix in server.
97 97 ## eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
98 98 ## And set your prefix like: `prefix = /custom_prefix`
99 99 ## be sure to also set beaker.session.cookie_path = /custom_prefix if you need
100 100 ## to make your cookies only work on prefix url
101 101 [filter:proxy-prefix]
102 102 use = egg:PasteDeploy#prefix
103 103 prefix = /
104 104
105 105 [app:main]
106 106 use = egg:rhodecode-enterprise-ce
107 107
108 108 ## enable proxy prefix middleware, defined above
109 109 #filter-with = proxy-prefix
110 110
111 111 # During development the we want to have the debug toolbar enabled
112 112 pyramid.includes =
113 113 pyramid_debugtoolbar
114 114 rhodecode.utils.debugtoolbar
115 115 rhodecode.lib.middleware.request_wrapper
116 116
117 117 pyramid.reload_templates = true
118 118
119 119 debugtoolbar.hosts = 0.0.0.0/0
120 120 debugtoolbar.exclude_prefixes =
121 121 /css
122 122 /fonts
123 123 /images
124 124 /js
125 125
126 126 ## RHODECODE PLUGINS ##
127 127 rhodecode.includes =
128 128 rhodecode.api
129 129
130 130
131 131 # api prefix url
132 132 rhodecode.api.url = /_admin/api
133 133
134 134
135 135 ## END RHODECODE PLUGINS ##
136 136
137 137 ## encryption key used to encrypt social plugin tokens,
138 138 ## remote_urls with credentials etc, if not set it defaults to
139 139 ## `beaker.session.secret`
140 140 #rhodecode.encrypted_values.secret =
141 141
142 142 ## decryption strict mode (enabled by default). It controls if decryption raises
143 143 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
144 144 #rhodecode.encrypted_values.strict = false
145 145
146 146 ## return gzipped responses from Rhodecode (static files/application)
147 147 gzip_responses = false
148 148
149 149 ## autogenerate javascript routes file on startup
150 150 generate_js_files = false
151 151
152 152 ## Optional Languages
153 153 ## en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
154 154 lang = en
155 155
156 156 ## perform a full repository scan on each server start, this should be
157 157 ## set to false after first startup, to allow faster server restarts.
158 158 startup.import_repos = false
159 159
160 160 ## Uncomment and set this path to use archive download cache.
161 161 ## Once enabled, generated archives will be cached at this location
162 162 ## and served from the cache during subsequent requests for the same archive of
163 163 ## the repository.
164 164 #archive_cache_dir = /tmp/tarballcache
165 165
166 166 ## change this to unique ID for security
167 167 app_instance_uuid = rc-production
168 168
169 169 ## cut off limit for large diffs (size in bytes)
170 170 cut_off_limit_diff = 1024000
171 171 cut_off_limit_file = 256000
172 172
173 173 ## use cache version of scm repo everywhere
174 174 vcs_full_cache = true
175 175
176 176 ## force https in RhodeCode, fixes https redirects, assumes it's always https
177 177 ## Normally this is controlled by proper http flags sent from http server
178 178 force_https = false
179 179
180 180 ## use Strict-Transport-Security headers
181 181 use_htsts = false
182 182
183 183 ## number of commits stats will parse on each iteration
184 184 commit_parse_limit = 25
185 185
186 186 ## git rev filter option, --all is the default filter, if you need to
187 187 ## hide all refs in changelog switch this to --branches --tags
188 188 git_rev_filter = --branches --tags
189 189
190 190 # Set to true if your repos are exposed using the dumb protocol
191 191 git_update_server_info = false
192 192
193 193 ## RSS/ATOM feed options
194 194 rss_cut_off_limit = 256000
195 195 rss_items_per_page = 10
196 196 rss_include_diff = false
197 197
198 198 ## gist URL alias, used to create nicer urls for gist. This should be an
199 199 ## url that does rewrites to _admin/gists/{gistid}.
200 200 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
201 201 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
202 202 gist_alias_url =
203 203
204 204 ## List of views (using glob pattern syntax) that AUTH TOKENS could be
205 205 ## used for access.
206 206 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
207 207 ## came from the the logged in user who own this authentication token.
208 208 ##
209 209 ## list of all views can be found under `_admin/permissions/auth_token_access`
210 210 ## The list should be "," separated and on a single line.
211 211 ##
212 212 ## Most common views to enable:
213 # ChangesetController:changeset_patch
214 # ChangesetController:changeset_raw
215 # RepoFilesView.repo_files_diff
216 # RepoFilesView.repo_archivefile
217 # RepoFilesView.repo_file_raw
213 # RepoCommitsView:repo_commit_download
214 # RepoCommitsView:repo_commit_patch
215 # RepoCommitsView:repo_commit_raw
216 # RepoFilesView:repo_files_diff
217 # RepoFilesView:repo_archivefile
218 # RepoFilesView:repo_file_raw
218 219 # GistView:*
219 220 api_access_controllers_whitelist =
220 221
221 222 ## default encoding used to convert from and to unicode
222 223 ## can be also a comma separated list of encoding in case of mixed encodings
223 224 default_encoding = UTF-8
224 225
225 226 ## instance-id prefix
226 227 ## a prefix key for this instance used for cache invalidation when running
227 228 ## multiple instances of rhodecode, make sure it's globally unique for
228 229 ## all running rhodecode instances. Leave empty if you don't use it
229 230 instance_id =
230 231
231 232 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
232 233 ## of an authentication plugin also if it is disabled by it's settings.
233 234 ## This could be useful if you are unable to log in to the system due to broken
234 235 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
235 236 ## module to log in again and fix the settings.
236 237 ##
237 238 ## Available builtin plugin IDs (hash is part of the ID):
238 239 ## egg:rhodecode-enterprise-ce#rhodecode
239 240 ## egg:rhodecode-enterprise-ce#pam
240 241 ## egg:rhodecode-enterprise-ce#ldap
241 242 ## egg:rhodecode-enterprise-ce#jasig_cas
242 243 ## egg:rhodecode-enterprise-ce#headers
243 244 ## egg:rhodecode-enterprise-ce#crowd
244 245 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
245 246
246 247 ## alternative return HTTP header for failed authentication. Default HTTP
247 248 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
248 249 ## handling that causing a series of failed authentication calls.
249 250 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
250 251 ## This will be served instead of default 401 on bad authnetication
251 252 auth_ret_code =
252 253
253 254 ## use special detection method when serving auth_ret_code, instead of serving
254 255 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
255 256 ## and then serve auth_ret_code to clients
256 257 auth_ret_code_detection = false
257 258
258 259 ## locking return code. When repository is locked return this HTTP code. 2XX
259 260 ## codes don't break the transactions while 4XX codes do
260 261 lock_ret_code = 423
261 262
262 263 ## allows to change the repository location in settings page
263 264 allow_repo_location_change = true
264 265
265 266 ## allows to setup custom hooks in settings page
266 267 allow_custom_hooks_settings = true
267 268
268 269 ## generated license token, goto license page in RhodeCode settings to obtain
269 270 ## new token
270 271 license_token =
271 272
272 273 ## supervisor connection uri, for managing supervisor and logs.
273 274 supervisor.uri =
274 275 ## supervisord group name/id we only want this RC instance to handle
275 276 supervisor.group_id = dev
276 277
277 278 ## Display extended labs settings
278 279 labs_settings_active = true
279 280
280 281 ####################################
281 282 ### CELERY CONFIG ####
282 283 ####################################
283 284 use_celery = false
284 285 broker.host = localhost
285 286 broker.vhost = rabbitmqhost
286 287 broker.port = 5672
287 288 broker.user = rabbitmq
288 289 broker.password = qweqwe
289 290
290 291 celery.imports = rhodecode.lib.celerylib.tasks
291 292
292 293 celery.result.backend = amqp
293 294 celery.result.dburi = amqp://
294 295 celery.result.serialier = json
295 296
296 297 #celery.send.task.error.emails = true
297 298 #celery.amqp.task.result.expires = 18000
298 299
299 300 celeryd.concurrency = 2
300 301 #celeryd.log.file = celeryd.log
301 302 celeryd.log.level = debug
302 303 celeryd.max.tasks.per.child = 1
303 304
304 305 ## tasks will never be sent to the queue, but executed locally instead.
305 306 celery.always.eager = false
306 307
307 308 ####################################
308 309 ### BEAKER CACHE ####
309 310 ####################################
310 311 # default cache dir for templates. Putting this into a ramdisk
311 312 ## can boost performance, eg. %(here)s/data_ramdisk
312 313 cache_dir = %(here)s/data
313 314
314 315 ## locking and default file storage for Beaker. Putting this into a ramdisk
315 316 ## can boost performance, eg. %(here)s/data_ramdisk/cache/beaker_data
316 317 beaker.cache.data_dir = %(here)s/data/cache/beaker_data
317 318 beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
318 319
319 320 beaker.cache.regions = super_short_term, short_term, long_term, sql_cache_short, auth_plugins, repo_cache_long
320 321
321 322 beaker.cache.super_short_term.type = memory
322 323 beaker.cache.super_short_term.expire = 10
323 324 beaker.cache.super_short_term.key_length = 256
324 325
325 326 beaker.cache.short_term.type = memory
326 327 beaker.cache.short_term.expire = 60
327 328 beaker.cache.short_term.key_length = 256
328 329
329 330 beaker.cache.long_term.type = memory
330 331 beaker.cache.long_term.expire = 36000
331 332 beaker.cache.long_term.key_length = 256
332 333
333 334 beaker.cache.sql_cache_short.type = memory
334 335 beaker.cache.sql_cache_short.expire = 10
335 336 beaker.cache.sql_cache_short.key_length = 256
336 337
337 338 ## default is memory cache, configure only if required
338 339 ## using multi-node or multi-worker setup
339 340 #beaker.cache.auth_plugins.type = ext:database
340 341 #beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock
341 342 #beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode
342 343 #beaker.cache.auth_plugins.url = mysql://root:secret@127.0.0.1/rhodecode
343 344 #beaker.cache.auth_plugins.sa.pool_recycle = 3600
344 345 #beaker.cache.auth_plugins.sa.pool_size = 10
345 346 #beaker.cache.auth_plugins.sa.max_overflow = 0
346 347
347 348 beaker.cache.repo_cache_long.type = memorylru_base
348 349 beaker.cache.repo_cache_long.max_items = 4096
349 350 beaker.cache.repo_cache_long.expire = 2592000
350 351
351 352 ## default is memorylru_base cache, configure only if required
352 353 ## using multi-node or multi-worker setup
353 354 #beaker.cache.repo_cache_long.type = ext:memcached
354 355 #beaker.cache.repo_cache_long.url = localhost:11211
355 356 #beaker.cache.repo_cache_long.expire = 1209600
356 357 #beaker.cache.repo_cache_long.key_length = 256
357 358
358 359 ####################################
359 360 ### BEAKER SESSION ####
360 361 ####################################
361 362
362 363 ## .session.type is type of storage options for the session, current allowed
363 364 ## types are file, ext:memcached, ext:database, and memory (default).
364 365 beaker.session.type = file
365 366 beaker.session.data_dir = %(here)s/data/sessions/data
366 367
367 368 ## db based session, fast, and allows easy management over logged in users
368 369 #beaker.session.type = ext:database
369 370 #beaker.session.table_name = db_session
370 371 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
371 372 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
372 373 #beaker.session.sa.pool_recycle = 3600
373 374 #beaker.session.sa.echo = false
374 375
375 376 beaker.session.key = rhodecode
376 377 beaker.session.secret = develop-rc-uytcxaz
377 378 beaker.session.lock_dir = %(here)s/data/sessions/lock
378 379
379 380 ## Secure encrypted cookie. Requires AES and AES python libraries
380 381 ## you must disable beaker.session.secret to use this
381 382 #beaker.session.encrypt_key = key_for_encryption
382 383 #beaker.session.validate_key = validation_key
383 384
384 385 ## sets session as invalid(also logging out user) if it haven not been
385 386 ## accessed for given amount of time in seconds
386 387 beaker.session.timeout = 2592000
387 388 beaker.session.httponly = true
388 389 ## Path to use for the cookie. Set to prefix if you use prefix middleware
389 390 #beaker.session.cookie_path = /custom_prefix
390 391
391 392 ## uncomment for https secure cookie
392 393 beaker.session.secure = false
393 394
394 395 ## auto save the session to not to use .save()
395 396 beaker.session.auto = false
396 397
397 398 ## default cookie expiration time in seconds, set to `true` to set expire
398 399 ## at browser close
399 400 #beaker.session.cookie_expires = 3600
400 401
401 402 ###################################
402 403 ## SEARCH INDEXING CONFIGURATION ##
403 404 ###################################
404 405 ## Full text search indexer is available in rhodecode-tools under
405 406 ## `rhodecode-tools index` command
406 407
407 408 ## WHOOSH Backend, doesn't require additional services to run
408 409 ## it works good with few dozen repos
409 410 search.module = rhodecode.lib.index.whoosh
410 411 search.location = %(here)s/data/index
411 412
412 413 ########################################
413 414 ### CHANNELSTREAM CONFIG ####
414 415 ########################################
415 416 ## channelstream enables persistent connections and live notification
416 417 ## in the system. It's also used by the chat system
417 418 channelstream.enabled = false
418 419
419 420 ## server address for channelstream server on the backend
420 421 channelstream.server = 127.0.0.1:9800
421 422
422 423 ## location of the channelstream server from outside world
423 424 ## use ws:// for http or wss:// for https. This address needs to be handled
424 425 ## by external HTTP server such as Nginx or Apache
425 426 ## see nginx/apache configuration examples in our docs
426 427 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
427 428 channelstream.secret = secret
428 429 channelstream.history.location = %(here)s/channelstream_history
429 430
430 431 ## Internal application path that Javascript uses to connect into.
431 432 ## If you use proxy-prefix the prefix should be added before /_channelstream
432 433 channelstream.proxy_path = /_channelstream
433 434
434 435
435 436 ###################################
436 437 ## APPENLIGHT CONFIG ##
437 438 ###################################
438 439
439 440 ## Appenlight is tailored to work with RhodeCode, see
440 441 ## http://appenlight.com for details how to obtain an account
441 442
442 443 ## appenlight integration enabled
443 444 appenlight = false
444 445
445 446 appenlight.server_url = https://api.appenlight.com
446 447 appenlight.api_key = YOUR_API_KEY
447 448 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
448 449
449 450 # used for JS client
450 451 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
451 452
452 453 ## TWEAK AMOUNT OF INFO SENT HERE
453 454
454 455 ## enables 404 error logging (default False)
455 456 appenlight.report_404 = false
456 457
457 458 ## time in seconds after request is considered being slow (default 1)
458 459 appenlight.slow_request_time = 1
459 460
460 461 ## record slow requests in application
461 462 ## (needs to be enabled for slow datastore recording and time tracking)
462 463 appenlight.slow_requests = true
463 464
464 465 ## enable hooking to application loggers
465 466 appenlight.logging = true
466 467
467 468 ## minimum log level for log capture
468 469 appenlight.logging.level = WARNING
469 470
470 471 ## send logs only from erroneous/slow requests
471 472 ## (saves API quota for intensive logging)
472 473 appenlight.logging_on_error = false
473 474
474 475 ## list of additonal keywords that should be grabbed from environ object
475 476 ## can be string with comma separated list of words in lowercase
476 477 ## (by default client will always send following info:
477 478 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
478 479 ## start with HTTP* this list be extended with additional keywords here
479 480 appenlight.environ_keys_whitelist =
480 481
481 482 ## list of keywords that should be blanked from request object
482 483 ## can be string with comma separated list of words in lowercase
483 484 ## (by default client will always blank keys that contain following words
484 485 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
485 486 ## this list be extended with additional keywords set here
486 487 appenlight.request_keys_blacklist =
487 488
488 489 ## list of namespaces that should be ignores when gathering log entries
489 490 ## can be string with comma separated list of namespaces
490 491 ## (by default the client ignores own entries: appenlight_client.client)
491 492 appenlight.log_namespace_blacklist =
492 493
493 494
494 495 ################################################################################
495 496 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
496 497 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
497 498 ## execute malicious code after an exception is raised. ##
498 499 ################################################################################
499 500 #set debug = false
500 501
501 502
502 503 ##############
503 504 ## STYLING ##
504 505 ##############
505 506 debug_style = true
506 507
507 508 ###########################################
508 509 ### MAIN RHODECODE DATABASE CONFIG ###
509 510 ###########################################
510 511 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
511 512 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
512 513 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode
513 514 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
514 515
515 516 # see sqlalchemy docs for other advanced settings
516 517
517 518 ## print the sql statements to output
518 519 sqlalchemy.db1.echo = false
519 520 ## recycle the connections after this amount of seconds
520 521 sqlalchemy.db1.pool_recycle = 3600
521 522 sqlalchemy.db1.convert_unicode = true
522 523
523 524 ## the number of connections to keep open inside the connection pool.
524 525 ## 0 indicates no limit
525 526 #sqlalchemy.db1.pool_size = 5
526 527
527 528 ## the number of connections to allow in connection pool "overflow", that is
528 529 ## connections that can be opened above and beyond the pool_size setting,
529 530 ## which defaults to five.
530 531 #sqlalchemy.db1.max_overflow = 10
531 532
532 533
533 534 ##################
534 535 ### VCS CONFIG ###
535 536 ##################
536 537 vcs.server.enable = true
537 538 vcs.server = localhost:9900
538 539
539 540 ## Web server connectivity protocol, responsible for web based VCS operatations
540 541 ## Available protocols are:
541 542 ## `http` - use http-rpc backend (default)
542 543 vcs.server.protocol = http
543 544
544 545 ## Push/Pull operations protocol, available options are:
545 546 ## `http` - use http-rpc backend (default)
546 547 ##
547 548 vcs.scm_app_implementation = http
548 549
549 550 ## Push/Pull operations hooks protocol, available options are:
550 551 ## `http` - use http-rpc backend (default)
551 552 vcs.hooks.protocol = http
552 553
553 554 vcs.server.log_level = debug
554 555 ## Start VCSServer with this instance as a subprocess, usefull for development
555 556 vcs.start_server = true
556 557
557 558 ## List of enabled VCS backends, available options are:
558 559 ## `hg` - mercurial
559 560 ## `git` - git
560 561 ## `svn` - subversion
561 562 vcs.backends = hg, git, svn
562 563
563 564 vcs.connection_timeout = 3600
564 565 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
565 566 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible
566 567 #vcs.svn.compatible_version = pre-1.8-compatible
567 568
568 569
569 570 ############################################################
570 571 ### Subversion proxy support (mod_dav_svn) ###
571 572 ### Maps RhodeCode repo groups into SVN paths for Apache ###
572 573 ############################################################
573 574 ## Enable or disable the config file generation.
574 575 svn.proxy.generate_config = false
575 576 ## Generate config file with `SVNListParentPath` set to `On`.
576 577 svn.proxy.list_parent_path = true
577 578 ## Set location and file name of generated config file.
578 579 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
579 580 ## Used as a prefix to the `Location` block in the generated config file.
580 581 ## In most cases it should be set to `/`.
581 582 svn.proxy.location_root = /
582 583 ## Command to reload the mod dav svn configuration on change.
583 584 ## Example: `/etc/init.d/apache2 reload`
584 585 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
585 586 ## If the timeout expires before the reload command finishes, the command will
586 587 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
587 588 #svn.proxy.reload_timeout = 10
588 589
589 590 ## Dummy marker to add new entries after.
590 591 ## Add any custom entries below. Please don't remove.
591 592 custom.conf = 1
592 593
593 594
594 595 ################################
595 596 ### LOGGING CONFIGURATION ####
596 597 ################################
597 598 [loggers]
598 599 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
599 600
600 601 [handlers]
601 602 keys = console, console_sql
602 603
603 604 [formatters]
604 605 keys = generic, color_formatter, color_formatter_sql
605 606
606 607 #############
607 608 ## LOGGERS ##
608 609 #############
609 610 [logger_root]
610 611 level = NOTSET
611 612 handlers = console
612 613
613 614 [logger_routes]
614 615 level = DEBUG
615 616 handlers =
616 617 qualname = routes.middleware
617 618 ## "level = DEBUG" logs the route matched and routing variables.
618 619 propagate = 1
619 620
620 621 [logger_beaker]
621 622 level = DEBUG
622 623 handlers =
623 624 qualname = beaker.container
624 625 propagate = 1
625 626
626 627 [logger_templates]
627 628 level = INFO
628 629 handlers =
629 630 qualname = pylons.templating
630 631 propagate = 1
631 632
632 633 [logger_rhodecode]
633 634 level = DEBUG
634 635 handlers =
635 636 qualname = rhodecode
636 637 propagate = 1
637 638
638 639 [logger_sqlalchemy]
639 640 level = INFO
640 641 handlers = console_sql
641 642 qualname = sqlalchemy.engine
642 643 propagate = 0
643 644
644 645 ##############
645 646 ## HANDLERS ##
646 647 ##############
647 648
648 649 [handler_console]
649 650 class = StreamHandler
650 651 args = (sys.stderr, )
651 652 level = DEBUG
652 653 formatter = color_formatter
653 654
654 655 [handler_console_sql]
655 656 class = StreamHandler
656 657 args = (sys.stderr, )
657 658 level = DEBUG
658 659 formatter = color_formatter_sql
659 660
660 661 ################
661 662 ## FORMATTERS ##
662 663 ################
663 664
664 665 [formatter_generic]
665 666 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
666 667 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
667 668 datefmt = %Y-%m-%d %H:%M:%S
668 669
669 670 [formatter_color_formatter]
670 671 class = rhodecode.lib.logging_formatter.ColorFormatter
671 672 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
672 673 datefmt = %Y-%m-%d %H:%M:%S
673 674
674 675 [formatter_color_formatter_sql]
675 676 class = rhodecode.lib.logging_formatter.ColorFormatterSql
676 677 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
677 678 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,646 +1,647 b''
1 1
2 2
3 3 ################################################################################
4 4 ## RHODECODE COMMUNITY EDITION CONFIGURATION ##
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10
11 11 ################################################################################
12 12 ## EMAIL CONFIGURATION ##
13 13 ## Uncomment and replace with the email address which should receive ##
14 14 ## any error reports after an application crash ##
15 15 ## Additionally these settings will be used by the RhodeCode mailing system ##
16 16 ################################################################################
17 17
18 18 ## prefix all emails subjects with given prefix, helps filtering out emails
19 19 #email_prefix = [RhodeCode]
20 20
21 21 ## email FROM address all mails will be sent
22 22 #app_email_from = rhodecode-noreply@localhost
23 23
24 24 ## Uncomment and replace with the address which should receive any error report
25 25 ## note: using appenlight for error handling doesn't need this to be uncommented
26 26 #email_to = admin@localhost
27 27
28 28 ## in case of Application errors, sent an error email form
29 29 #error_email_from = rhodecode_error@localhost
30 30
31 31 ## additional error message to be send in case of server crash
32 32 #error_message =
33 33
34 34
35 35 #smtp_server = mail.server.com
36 36 #smtp_username =
37 37 #smtp_password =
38 38 #smtp_port =
39 39 #smtp_use_tls = false
40 40 #smtp_use_ssl = true
41 41 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
42 42 #smtp_auth =
43 43
44 44 [server:main]
45 45 ## COMMON ##
46 46 host = 127.0.0.1
47 47 port = 5000
48 48
49 49 ##################################
50 50 ## WAITRESS WSGI SERVER ##
51 51 ## Recommended for Development ##
52 52 ##################################
53 53
54 54 #use = egg:waitress#main
55 55 ## number of worker threads
56 56 #threads = 5
57 57 ## MAX BODY SIZE 100GB
58 58 #max_request_body_size = 107374182400
59 59 ## Use poll instead of select, fixes file descriptors limits problems.
60 60 ## May not work on old windows systems.
61 61 #asyncore_use_poll = true
62 62
63 63
64 64 ##########################
65 65 ## GUNICORN WSGI SERVER ##
66 66 ##########################
67 67 ## run with gunicorn --log-config rhodecode.ini --paste rhodecode.ini
68 68
69 69 use = egg:gunicorn#main
70 70 ## Sets the number of process workers. You must set `instance_id = *`
71 71 ## when this option is set to more than one worker, recommended
72 72 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
73 73 ## The `instance_id = *` must be set in the [app:main] section below
74 74 workers = 2
75 75 ## number of threads for each of the worker, must be set to 1 for gevent
76 76 ## generally recommened to be at 1
77 77 #threads = 1
78 78 ## process name
79 79 proc_name = rhodecode
80 80 ## type of worker class, one of sync, gevent
81 81 ## recommended for bigger setup is using of of other than sync one
82 82 worker_class = sync
83 83 ## The maximum number of simultaneous clients. Valid only for Gevent
84 84 #worker_connections = 10
85 85 ## max number of requests that worker will handle before being gracefully
86 86 ## restarted, could prevent memory leaks
87 87 max_requests = 1000
88 88 max_requests_jitter = 30
89 89 ## amount of time a worker can spend with handling a request before it
90 90 ## gets killed and restarted. Set to 6hrs
91 91 timeout = 21600
92 92
93 93
94 94 ## prefix middleware for RhodeCode.
95 95 ## recommended when using proxy setup.
96 96 ## allows to set RhodeCode under a prefix in server.
97 97 ## eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
98 98 ## And set your prefix like: `prefix = /custom_prefix`
99 99 ## be sure to also set beaker.session.cookie_path = /custom_prefix if you need
100 100 ## to make your cookies only work on prefix url
101 101 [filter:proxy-prefix]
102 102 use = egg:PasteDeploy#prefix
103 103 prefix = /
104 104
105 105 [app:main]
106 106 use = egg:rhodecode-enterprise-ce
107 107
108 108 ## enable proxy prefix middleware, defined above
109 109 #filter-with = proxy-prefix
110 110
111 111 ## encryption key used to encrypt social plugin tokens,
112 112 ## remote_urls with credentials etc, if not set it defaults to
113 113 ## `beaker.session.secret`
114 114 #rhodecode.encrypted_values.secret =
115 115
116 116 ## decryption strict mode (enabled by default). It controls if decryption raises
117 117 ## `SignatureVerificationError` in case of wrong key, or damaged encryption data.
118 118 #rhodecode.encrypted_values.strict = false
119 119
120 120 ## return gzipped responses from Rhodecode (static files/application)
121 121 gzip_responses = false
122 122
123 123 ## autogenerate javascript routes file on startup
124 124 generate_js_files = false
125 125
126 126 ## Optional Languages
127 127 ## en(default), be, de, es, fr, it, ja, pl, pt, ru, zh
128 128 lang = en
129 129
130 130 ## perform a full repository scan on each server start, this should be
131 131 ## set to false after first startup, to allow faster server restarts.
132 132 startup.import_repos = false
133 133
134 134 ## Uncomment and set this path to use archive download cache.
135 135 ## Once enabled, generated archives will be cached at this location
136 136 ## and served from the cache during subsequent requests for the same archive of
137 137 ## the repository.
138 138 #archive_cache_dir = /tmp/tarballcache
139 139
140 140 ## change this to unique ID for security
141 141 app_instance_uuid = rc-production
142 142
143 143 ## cut off limit for large diffs (size in bytes)
144 144 cut_off_limit_diff = 1024000
145 145 cut_off_limit_file = 256000
146 146
147 147 ## use cache version of scm repo everywhere
148 148 vcs_full_cache = true
149 149
150 150 ## force https in RhodeCode, fixes https redirects, assumes it's always https
151 151 ## Normally this is controlled by proper http flags sent from http server
152 152 force_https = false
153 153
154 154 ## use Strict-Transport-Security headers
155 155 use_htsts = false
156 156
157 157 ## number of commits stats will parse on each iteration
158 158 commit_parse_limit = 25
159 159
160 160 ## git rev filter option, --all is the default filter, if you need to
161 161 ## hide all refs in changelog switch this to --branches --tags
162 162 git_rev_filter = --branches --tags
163 163
164 164 # Set to true if your repos are exposed using the dumb protocol
165 165 git_update_server_info = false
166 166
167 167 ## RSS/ATOM feed options
168 168 rss_cut_off_limit = 256000
169 169 rss_items_per_page = 10
170 170 rss_include_diff = false
171 171
172 172 ## gist URL alias, used to create nicer urls for gist. This should be an
173 173 ## url that does rewrites to _admin/gists/{gistid}.
174 174 ## example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
175 175 ## RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
176 176 gist_alias_url =
177 177
178 178 ## List of views (using glob pattern syntax) that AUTH TOKENS could be
179 179 ## used for access.
180 180 ## Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
181 181 ## came from the the logged in user who own this authentication token.
182 182 ##
183 183 ## list of all views can be found under `_admin/permissions/auth_token_access`
184 184 ## The list should be "," separated and on a single line.
185 185 ##
186 186 ## Most common views to enable:
187 # ChangesetController:changeset_patch
188 # ChangesetController:changeset_raw
189 # RepoFilesView.repo_files_diff
190 # RepoFilesView.repo_archivefile
191 # RepoFilesView.repo_file_raw
187 # RepoCommitsView:repo_commit_download
188 # RepoCommitsView:repo_commit_patch
189 # RepoCommitsView:repo_commit_raw
190 # RepoFilesView:repo_files_diff
191 # RepoFilesView:repo_archivefile
192 # RepoFilesView:repo_file_raw
192 193 # GistView:*
193 194 api_access_controllers_whitelist =
194 195
195 196 ## default encoding used to convert from and to unicode
196 197 ## can be also a comma separated list of encoding in case of mixed encodings
197 198 default_encoding = UTF-8
198 199
199 200 ## instance-id prefix
200 201 ## a prefix key for this instance used for cache invalidation when running
201 202 ## multiple instances of rhodecode, make sure it's globally unique for
202 203 ## all running rhodecode instances. Leave empty if you don't use it
203 204 instance_id =
204 205
205 206 ## Fallback authentication plugin. Set this to a plugin ID to force the usage
206 207 ## of an authentication plugin also if it is disabled by it's settings.
207 208 ## This could be useful if you are unable to log in to the system due to broken
208 209 ## authentication settings. Then you can enable e.g. the internal rhodecode auth
209 210 ## module to log in again and fix the settings.
210 211 ##
211 212 ## Available builtin plugin IDs (hash is part of the ID):
212 213 ## egg:rhodecode-enterprise-ce#rhodecode
213 214 ## egg:rhodecode-enterprise-ce#pam
214 215 ## egg:rhodecode-enterprise-ce#ldap
215 216 ## egg:rhodecode-enterprise-ce#jasig_cas
216 217 ## egg:rhodecode-enterprise-ce#headers
217 218 ## egg:rhodecode-enterprise-ce#crowd
218 219 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
219 220
220 221 ## alternative return HTTP header for failed authentication. Default HTTP
221 222 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
222 223 ## handling that causing a series of failed authentication calls.
223 224 ## Set this variable to 403 to return HTTPForbidden, or any other HTTP code
224 225 ## This will be served instead of default 401 on bad authnetication
225 226 auth_ret_code =
226 227
227 228 ## use special detection method when serving auth_ret_code, instead of serving
228 229 ## ret_code directly, use 401 initially (Which triggers credentials prompt)
229 230 ## and then serve auth_ret_code to clients
230 231 auth_ret_code_detection = false
231 232
232 233 ## locking return code. When repository is locked return this HTTP code. 2XX
233 234 ## codes don't break the transactions while 4XX codes do
234 235 lock_ret_code = 423
235 236
236 237 ## allows to change the repository location in settings page
237 238 allow_repo_location_change = true
238 239
239 240 ## allows to setup custom hooks in settings page
240 241 allow_custom_hooks_settings = true
241 242
242 243 ## generated license token, goto license page in RhodeCode settings to obtain
243 244 ## new token
244 245 license_token =
245 246
246 247 ## supervisor connection uri, for managing supervisor and logs.
247 248 supervisor.uri =
248 249 ## supervisord group name/id we only want this RC instance to handle
249 250 supervisor.group_id = prod
250 251
251 252 ## Display extended labs settings
252 253 labs_settings_active = true
253 254
254 255 ####################################
255 256 ### CELERY CONFIG ####
256 257 ####################################
257 258 use_celery = false
258 259 broker.host = localhost
259 260 broker.vhost = rabbitmqhost
260 261 broker.port = 5672
261 262 broker.user = rabbitmq
262 263 broker.password = qweqwe
263 264
264 265 celery.imports = rhodecode.lib.celerylib.tasks
265 266
266 267 celery.result.backend = amqp
267 268 celery.result.dburi = amqp://
268 269 celery.result.serialier = json
269 270
270 271 #celery.send.task.error.emails = true
271 272 #celery.amqp.task.result.expires = 18000
272 273
273 274 celeryd.concurrency = 2
274 275 #celeryd.log.file = celeryd.log
275 276 celeryd.log.level = debug
276 277 celeryd.max.tasks.per.child = 1
277 278
278 279 ## tasks will never be sent to the queue, but executed locally instead.
279 280 celery.always.eager = false
280 281
281 282 ####################################
282 283 ### BEAKER CACHE ####
283 284 ####################################
284 285 # default cache dir for templates. Putting this into a ramdisk
285 286 ## can boost performance, eg. %(here)s/data_ramdisk
286 287 cache_dir = %(here)s/data
287 288
288 289 ## locking and default file storage for Beaker. Putting this into a ramdisk
289 290 ## can boost performance, eg. %(here)s/data_ramdisk/cache/beaker_data
290 291 beaker.cache.data_dir = %(here)s/data/cache/beaker_data
291 292 beaker.cache.lock_dir = %(here)s/data/cache/beaker_lock
292 293
293 294 beaker.cache.regions = super_short_term, short_term, long_term, sql_cache_short, auth_plugins, repo_cache_long
294 295
295 296 beaker.cache.super_short_term.type = memory
296 297 beaker.cache.super_short_term.expire = 10
297 298 beaker.cache.super_short_term.key_length = 256
298 299
299 300 beaker.cache.short_term.type = memory
300 301 beaker.cache.short_term.expire = 60
301 302 beaker.cache.short_term.key_length = 256
302 303
303 304 beaker.cache.long_term.type = memory
304 305 beaker.cache.long_term.expire = 36000
305 306 beaker.cache.long_term.key_length = 256
306 307
307 308 beaker.cache.sql_cache_short.type = memory
308 309 beaker.cache.sql_cache_short.expire = 10
309 310 beaker.cache.sql_cache_short.key_length = 256
310 311
311 312 ## default is memory cache, configure only if required
312 313 ## using multi-node or multi-worker setup
313 314 #beaker.cache.auth_plugins.type = ext:database
314 315 #beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock
315 316 #beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode
316 317 #beaker.cache.auth_plugins.url = mysql://root:secret@127.0.0.1/rhodecode
317 318 #beaker.cache.auth_plugins.sa.pool_recycle = 3600
318 319 #beaker.cache.auth_plugins.sa.pool_size = 10
319 320 #beaker.cache.auth_plugins.sa.max_overflow = 0
320 321
321 322 beaker.cache.repo_cache_long.type = memorylru_base
322 323 beaker.cache.repo_cache_long.max_items = 4096
323 324 beaker.cache.repo_cache_long.expire = 2592000
324 325
325 326 ## default is memorylru_base cache, configure only if required
326 327 ## using multi-node or multi-worker setup
327 328 #beaker.cache.repo_cache_long.type = ext:memcached
328 329 #beaker.cache.repo_cache_long.url = localhost:11211
329 330 #beaker.cache.repo_cache_long.expire = 1209600
330 331 #beaker.cache.repo_cache_long.key_length = 256
331 332
332 333 ####################################
333 334 ### BEAKER SESSION ####
334 335 ####################################
335 336
336 337 ## .session.type is type of storage options for the session, current allowed
337 338 ## types are file, ext:memcached, ext:database, and memory (default).
338 339 beaker.session.type = file
339 340 beaker.session.data_dir = %(here)s/data/sessions/data
340 341
341 342 ## db based session, fast, and allows easy management over logged in users
342 343 #beaker.session.type = ext:database
343 344 #beaker.session.table_name = db_session
344 345 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
345 346 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
346 347 #beaker.session.sa.pool_recycle = 3600
347 348 #beaker.session.sa.echo = false
348 349
349 350 beaker.session.key = rhodecode
350 351 beaker.session.secret = production-rc-uytcxaz
351 352 beaker.session.lock_dir = %(here)s/data/sessions/lock
352 353
353 354 ## Secure encrypted cookie. Requires AES and AES python libraries
354 355 ## you must disable beaker.session.secret to use this
355 356 #beaker.session.encrypt_key = key_for_encryption
356 357 #beaker.session.validate_key = validation_key
357 358
358 359 ## sets session as invalid(also logging out user) if it haven not been
359 360 ## accessed for given amount of time in seconds
360 361 beaker.session.timeout = 2592000
361 362 beaker.session.httponly = true
362 363 ## Path to use for the cookie. Set to prefix if you use prefix middleware
363 364 #beaker.session.cookie_path = /custom_prefix
364 365
365 366 ## uncomment for https secure cookie
366 367 beaker.session.secure = false
367 368
368 369 ## auto save the session to not to use .save()
369 370 beaker.session.auto = false
370 371
371 372 ## default cookie expiration time in seconds, set to `true` to set expire
372 373 ## at browser close
373 374 #beaker.session.cookie_expires = 3600
374 375
375 376 ###################################
376 377 ## SEARCH INDEXING CONFIGURATION ##
377 378 ###################################
378 379 ## Full text search indexer is available in rhodecode-tools under
379 380 ## `rhodecode-tools index` command
380 381
381 382 ## WHOOSH Backend, doesn't require additional services to run
382 383 ## it works good with few dozen repos
383 384 search.module = rhodecode.lib.index.whoosh
384 385 search.location = %(here)s/data/index
385 386
386 387 ########################################
387 388 ### CHANNELSTREAM CONFIG ####
388 389 ########################################
389 390 ## channelstream enables persistent connections and live notification
390 391 ## in the system. It's also used by the chat system
391 392 channelstream.enabled = false
392 393
393 394 ## server address for channelstream server on the backend
394 395 channelstream.server = 127.0.0.1:9800
395 396
396 397 ## location of the channelstream server from outside world
397 398 ## use ws:// for http or wss:// for https. This address needs to be handled
398 399 ## by external HTTP server such as Nginx or Apache
399 400 ## see nginx/apache configuration examples in our docs
400 401 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
401 402 channelstream.secret = secret
402 403 channelstream.history.location = %(here)s/channelstream_history
403 404
404 405 ## Internal application path that Javascript uses to connect into.
405 406 ## If you use proxy-prefix the prefix should be added before /_channelstream
406 407 channelstream.proxy_path = /_channelstream
407 408
408 409
409 410 ###################################
410 411 ## APPENLIGHT CONFIG ##
411 412 ###################################
412 413
413 414 ## Appenlight is tailored to work with RhodeCode, see
414 415 ## http://appenlight.com for details how to obtain an account
415 416
416 417 ## appenlight integration enabled
417 418 appenlight = false
418 419
419 420 appenlight.server_url = https://api.appenlight.com
420 421 appenlight.api_key = YOUR_API_KEY
421 422 #appenlight.transport_config = https://api.appenlight.com?threaded=1&timeout=5
422 423
423 424 # used for JS client
424 425 appenlight.api_public_key = YOUR_API_PUBLIC_KEY
425 426
426 427 ## TWEAK AMOUNT OF INFO SENT HERE
427 428
428 429 ## enables 404 error logging (default False)
429 430 appenlight.report_404 = false
430 431
431 432 ## time in seconds after request is considered being slow (default 1)
432 433 appenlight.slow_request_time = 1
433 434
434 435 ## record slow requests in application
435 436 ## (needs to be enabled for slow datastore recording and time tracking)
436 437 appenlight.slow_requests = true
437 438
438 439 ## enable hooking to application loggers
439 440 appenlight.logging = true
440 441
441 442 ## minimum log level for log capture
442 443 appenlight.logging.level = WARNING
443 444
444 445 ## send logs only from erroneous/slow requests
445 446 ## (saves API quota for intensive logging)
446 447 appenlight.logging_on_error = false
447 448
448 449 ## list of additonal keywords that should be grabbed from environ object
449 450 ## can be string with comma separated list of words in lowercase
450 451 ## (by default client will always send following info:
451 452 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
452 453 ## start with HTTP* this list be extended with additional keywords here
453 454 appenlight.environ_keys_whitelist =
454 455
455 456 ## list of keywords that should be blanked from request object
456 457 ## can be string with comma separated list of words in lowercase
457 458 ## (by default client will always blank keys that contain following words
458 459 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
459 460 ## this list be extended with additional keywords set here
460 461 appenlight.request_keys_blacklist =
461 462
462 463 ## list of namespaces that should be ignores when gathering log entries
463 464 ## can be string with comma separated list of namespaces
464 465 ## (by default the client ignores own entries: appenlight_client.client)
465 466 appenlight.log_namespace_blacklist =
466 467
467 468
468 469 ################################################################################
469 470 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
470 471 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
471 472 ## execute malicious code after an exception is raised. ##
472 473 ################################################################################
473 474 set debug = false
474 475
475 476
476 477 ###########################################
477 478 ### MAIN RHODECODE DATABASE CONFIG ###
478 479 ###########################################
479 480 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
480 481 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
481 482 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode
482 483 sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
483 484
484 485 # see sqlalchemy docs for other advanced settings
485 486
486 487 ## print the sql statements to output
487 488 sqlalchemy.db1.echo = false
488 489 ## recycle the connections after this amount of seconds
489 490 sqlalchemy.db1.pool_recycle = 3600
490 491 sqlalchemy.db1.convert_unicode = true
491 492
492 493 ## the number of connections to keep open inside the connection pool.
493 494 ## 0 indicates no limit
494 495 #sqlalchemy.db1.pool_size = 5
495 496
496 497 ## the number of connections to allow in connection pool "overflow", that is
497 498 ## connections that can be opened above and beyond the pool_size setting,
498 499 ## which defaults to five.
499 500 #sqlalchemy.db1.max_overflow = 10
500 501
501 502
502 503 ##################
503 504 ### VCS CONFIG ###
504 505 ##################
505 506 vcs.server.enable = true
506 507 vcs.server = localhost:9900
507 508
508 509 ## Web server connectivity protocol, responsible for web based VCS operatations
509 510 ## Available protocols are:
510 511 ## `http` - use http-rpc backend (default)
511 512 vcs.server.protocol = http
512 513
513 514 ## Push/Pull operations protocol, available options are:
514 515 ## `http` - use http-rpc backend (default)
515 516 ##
516 517 vcs.scm_app_implementation = http
517 518
518 519 ## Push/Pull operations hooks protocol, available options are:
519 520 ## `http` - use http-rpc backend (default)
520 521 vcs.hooks.protocol = http
521 522
522 523 vcs.server.log_level = info
523 524 ## Start VCSServer with this instance as a subprocess, usefull for development
524 525 vcs.start_server = false
525 526
526 527 ## List of enabled VCS backends, available options are:
527 528 ## `hg` - mercurial
528 529 ## `git` - git
529 530 ## `svn` - subversion
530 531 vcs.backends = hg, git, svn
531 532
532 533 vcs.connection_timeout = 3600
533 534 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
534 535 ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible
535 536 #vcs.svn.compatible_version = pre-1.8-compatible
536 537
537 538
538 539 ############################################################
539 540 ### Subversion proxy support (mod_dav_svn) ###
540 541 ### Maps RhodeCode repo groups into SVN paths for Apache ###
541 542 ############################################################
542 543 ## Enable or disable the config file generation.
543 544 svn.proxy.generate_config = false
544 545 ## Generate config file with `SVNListParentPath` set to `On`.
545 546 svn.proxy.list_parent_path = true
546 547 ## Set location and file name of generated config file.
547 548 svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
548 549 ## Used as a prefix to the `Location` block in the generated config file.
549 550 ## In most cases it should be set to `/`.
550 551 svn.proxy.location_root = /
551 552 ## Command to reload the mod dav svn configuration on change.
552 553 ## Example: `/etc/init.d/apache2 reload`
553 554 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
554 555 ## If the timeout expires before the reload command finishes, the command will
555 556 ## be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
556 557 #svn.proxy.reload_timeout = 10
557 558
558 559 ## Dummy marker to add new entries after.
559 560 ## Add any custom entries below. Please don't remove.
560 561 custom.conf = 1
561 562
562 563
563 564 ################################
564 565 ### LOGGING CONFIGURATION ####
565 566 ################################
566 567 [loggers]
567 568 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
568 569
569 570 [handlers]
570 571 keys = console, console_sql
571 572
572 573 [formatters]
573 574 keys = generic, color_formatter, color_formatter_sql
574 575
575 576 #############
576 577 ## LOGGERS ##
577 578 #############
578 579 [logger_root]
579 580 level = NOTSET
580 581 handlers = console
581 582
582 583 [logger_routes]
583 584 level = DEBUG
584 585 handlers =
585 586 qualname = routes.middleware
586 587 ## "level = DEBUG" logs the route matched and routing variables.
587 588 propagate = 1
588 589
589 590 [logger_beaker]
590 591 level = DEBUG
591 592 handlers =
592 593 qualname = beaker.container
593 594 propagate = 1
594 595
595 596 [logger_templates]
596 597 level = INFO
597 598 handlers =
598 599 qualname = pylons.templating
599 600 propagate = 1
600 601
601 602 [logger_rhodecode]
602 603 level = DEBUG
603 604 handlers =
604 605 qualname = rhodecode
605 606 propagate = 1
606 607
607 608 [logger_sqlalchemy]
608 609 level = INFO
609 610 handlers = console_sql
610 611 qualname = sqlalchemy.engine
611 612 propagate = 0
612 613
613 614 ##############
614 615 ## HANDLERS ##
615 616 ##############
616 617
617 618 [handler_console]
618 619 class = StreamHandler
619 620 args = (sys.stderr, )
620 621 level = INFO
621 622 formatter = generic
622 623
623 624 [handler_console_sql]
624 625 class = StreamHandler
625 626 args = (sys.stderr, )
626 627 level = WARN
627 628 formatter = generic
628 629
629 630 ################
630 631 ## FORMATTERS ##
631 632 ################
632 633
633 634 [formatter_generic]
634 635 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
635 636 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
636 637 datefmt = %Y-%m-%d %H:%M:%S
637 638
638 639 [formatter_color_formatter]
639 640 class = rhodecode.lib.logging_formatter.ColorFormatter
640 641 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
641 642 datefmt = %Y-%m-%d %H:%M:%S
642 643
643 644 [formatter_color_formatter_sql]
644 645 class = rhodecode.lib.logging_formatter.ColorFormatterSql
645 646 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
646 647 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,207 +1,207 b''
1 1 .. _api:
2 2
3 3 API Documentation
4 4 =================
5 5
6 6 The |RCE| API uses a single scheme for calling all API methods. The API is
7 7 implemented with JSON protocol in both directions. To send API requests to
8 8 your instance of |RCE|, use the following URL format
9 9 ``<your_server>/_admin``
10 10
11 11 .. note::
12 12
13 13 To use the API, you should configure the :file:`~/.rhoderc` file with
14 14 access details per instance. For more information, see
15 15 :ref:`config-rhoderc`.
16 16
17 17
18 18 API ACCESS FOR WEB VIEWS
19 19 ------------------------
20 20
21 21 API access can also be turned on for each web view in |RCE| that is
22 22 decorated with a `@LoginRequired` decorator. To enable API access, change
23 23 the standard login decorator to `@LoginRequired(api_access=True)`.
24 24
25 25 From |RCM| version 1.7.0 you can configure a white list
26 26 of views that have API access enabled by default. To enable these,
27 27 edit the |RCM| configuration ``.ini`` file. The default location is:
28 28
29 29 * |RCM| Pre-2.2.7 :file:`root/rhodecode/data/production.ini`
30 30 * |RCM| 3.0 :file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
31 31
32 32 To configure the white list, edit this section of the file. In this
33 33 configuration example, API access is granted to the patch/diff raw file and
34 34 archive.
35 35
36 36 .. code-block:: ini
37 37
38 38 ## List of controllers (using glob syntax) that AUTH TOKENS could be used for access.
39 39 ## Adding ?auth_token = <token> to the url authenticates this request as if it
40 40 ## came from the the logged in user who own this authentication token.
41 41 ##
42 42 ## Syntax is <ControllerClass>:<function_pattern>.
43 43 ## The list should be "," separated and on a single line.
44 44 ##
45 api_access_controllers_whitelist = ChangesetController:changeset_patch,ChangesetController:changeset_raw,ilesController:raw,FilesController:archivefile,
45 api_access_controllers_whitelist = RepoCommitsView:repo_commit_raw,RepoCommitsView:repo_commit_patch,RepoCommitsView:repo_commit_download
46 46
47 47 After this change, a |RCE| view can be accessed without login by adding a
48 48 GET parameter ``?auth_token=<auth_token>`` to a url. For example to
49 49 access the raw diff.
50 50
51 51 .. code-block:: html
52 52
53 53 http://<server>/<repo>/changeset-diff/<sha>?auth_token=<auth_token>
54 54
55 55 By default this is only enabled on RSS/ATOM feed views. Exposing raw diffs is a
56 56 good way to integrate with 3rd party services like code review, or build farms
57 57 that could download archives.
58 58
59 59 API ACCESS
60 60 ----------
61 61
62 62 All clients are required to send JSON-RPC spec JSON data.
63 63
64 64 .. code-block:: bash
65 65
66 66 {
67 67 "id:"<id>",
68 68 "auth_token":"<auth_token>",
69 69 "method":"<method_name>",
70 70 "args":{"<arg_key>":"<arg_val>"}
71 71 }
72 72
73 73 Example call for auto pulling from remote repositories using curl:
74 74
75 75 .. code-block:: bash
76 76
77 77 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,
78 78 "auth_token":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull", "args":{"repoid":"CPython"}}'
79 79
80 80 Provide those parameters:
81 81 - **id** A value of any type, which is used to match the response with the
82 82 request that it is replying to.
83 83 - **auth_token** for access and permission validation.
84 84 - **method** is name of method to call
85 85 - **args** is an ``key:value`` list of arguments to pass to method
86 86
87 87 .. note::
88 88
89 89 To get your |authtoken|, from the |RCE| interface,
90 90 go to:
91 91 :menuselection:`username --> My account --> Auth tokens`
92 92
93 93 For security reasons you should always create a dedicated |authtoken| for
94 94 API use only.
95 95
96 96
97 97 The |RCE| API will always return a JSON-RPC response:
98 98
99 99 .. code-block:: bash
100 100
101 101 {
102 102 "id": <id>, # matching id sent by request
103 103 "result": "<result>"|null, # JSON formatted result, null if any errors
104 104 "error": "null"|<error_message> # JSON formatted error (if any)
105 105 }
106 106
107 107 All responses from API will be with `HTTP/1.0 200 OK` status code.
108 108 If there is an error when calling the API, the *error* key will contain a
109 109 failure description and the *result* will be `null`.
110 110
111 111 API CLIENT
112 112 ----------
113 113
114 114 To install the |RCE| API, see :ref:`install-tools`. To configure the API per
115 115 instance, see the :ref:`rc-tools` section as you need to configure a
116 116 :file:`~/.rhoderc` file with your |authtokens|.
117 117
118 118 Once you have set up your instance API access, use the following examples to
119 119 get started.
120 120
121 121 .. code-block:: bash
122 122
123 123 # Getting the 'rhodecode' repository
124 124 # from a RhodeCode Enterprise instance
125 125 rhodecode-api --instance-name=enterprise-1 get_repo repoid:rhodecode
126 126
127 127 Calling method get_repo => http://127.0.0.1:5000
128 128 Server response
129 129 {
130 130 <json data>
131 131 }
132 132
133 133 # Creating a new mercurial repository called 'brand-new'
134 134 # with a description 'Repo-description'
135 135 rhodecode-api --instance-name=enterprise-1 create_repo repo_name:brand-new repo_type:hg description:Repo-description
136 136 {
137 137 "error": null,
138 138 "id": 1110,
139 139 "result": {
140 140 "msg": "Created new repository `brand-new`",
141 141 "success": true,
142 142 "task": null
143 143 }
144 144 }
145 145
146 146 A broken example, what not to do.
147 147
148 148 .. code-block:: bash
149 149
150 150 # A call missing the required arguments
151 151 # and not specifying the instance
152 152 rhodecode-api get_repo
153 153
154 154 Calling method get_repo => http://127.0.0.1:5000
155 155 Server response
156 156 "Missing non optional `repoid` arg in JSON DATA"
157 157
158 158 You can specify pure JSON using the ``--format`` parameter.
159 159
160 160 .. code-block:: bash
161 161
162 162 rhodecode-api --format=json get_repo repoid:rhodecode
163 163
164 164 In such case only output that this function shows is pure JSON, we can use that
165 165 and pipe output to some json formatter.
166 166
167 167 If output is in pure JSON format, you can pipe output to a JSON formatter.
168 168
169 169 .. code-block:: bash
170 170
171 171 rhodecode-api --instance-name=enterprise-1 --format=json get_repo repoid:rhodecode | python -m json.tool
172 172
173 173 API METHODS
174 174 -----------
175 175
176 176 Each method by default required following arguments.
177 177
178 178 .. code-block:: bash
179 179
180 180 id : "<id_for_response>"
181 181 auth_token : "<auth_token>"
182 182 method : "<method name>"
183 183 args : {}
184 184
185 185 Use each **param** from docs and put it in args, Optional parameters
186 186 are not required in args.
187 187
188 188 .. code-block:: bash
189 189
190 190 args: {"repoid": "rhodecode"}
191 191
192 192 .. Note: From this point on things are generated by the script in
193 193 `scripts/fabfile.py`. To change things below, update the docstrings in the
194 194 ApiController.
195 195
196 196 .. --- API DEFS MARKER ---
197 197 .. toctree::
198 198
199 199 methods/license-methods
200 200 methods/deprecated-methods
201 201 methods/gist-methods
202 202 methods/pull-request-methods
203 203 methods/repo-methods
204 204 methods/repo-group-methods
205 205 methods/server-methods
206 206 methods/user-methods
207 207 methods/user-group-methods
@@ -1,304 +1,304 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import re
22 22 import logging
23 23
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, \
29 29 HasRepoGroupPermissionAnyDecorator
30 30 from rhodecode.lib.index import searcher_from_config
31 31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.model.db import func, Repository, RepoGroup
34 34 from rhodecode.model.repo import RepoModel
35 35 from rhodecode.model.repo_group import RepoGroupModel
36 36 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
37 37 from rhodecode.model.user import UserModel
38 38 from rhodecode.model.user_group import UserGroupModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class HomeView(BaseAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 c.user = c.auth_user.get_instance()
48 48 self._register_global_c(c)
49 49 return c
50 50
51 51 @LoginRequired()
52 52 @view_config(
53 53 route_name='user_autocomplete_data', request_method='GET',
54 54 renderer='json_ext', xhr=True)
55 55 def user_autocomplete_data(self):
56 56 query = self.request.GET.get('query')
57 57 active = str2bool(self.request.GET.get('active') or True)
58 58 include_groups = str2bool(self.request.GET.get('user_groups'))
59 59 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
60 60 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
61 61
62 62 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
63 63 query, active, include_groups)
64 64
65 65 _users = UserModel().get_users(
66 66 name_contains=query, only_active=active)
67 67
68 68 def maybe_skip_default_user(usr):
69 69 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
70 70 return False
71 71 return True
72 72 _users = filter(maybe_skip_default_user, _users)
73 73
74 74 if include_groups:
75 75 # extend with user groups
76 76 _user_groups = UserGroupModel().get_user_groups(
77 77 name_contains=query, only_active=active,
78 78 expand_groups=expand_groups)
79 79 _users = _users + _user_groups
80 80
81 81 return {'suggestions': _users}
82 82
83 83 @LoginRequired()
84 84 @NotAnonymous()
85 85 @view_config(
86 86 route_name='user_group_autocomplete_data', request_method='GET',
87 87 renderer='json_ext', xhr=True)
88 88 def user_group_autocomplete_data(self):
89 89 query = self.request.GET.get('query')
90 90 active = str2bool(self.request.GET.get('active') or True)
91 91 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
92 92
93 93 log.debug('generating user group list, query:%s, active:%s',
94 94 query, active)
95 95
96 96 _user_groups = UserGroupModel().get_user_groups(
97 97 name_contains=query, only_active=active,
98 98 expand_groups=expand_groups)
99 99 _user_groups = _user_groups
100 100
101 101 return {'suggestions': _user_groups}
102 102
103 103 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
104 104 query = Repository.query()\
105 105 .order_by(func.length(Repository.repo_name))\
106 106 .order_by(Repository.repo_name)
107 107
108 108 if repo_type:
109 109 query = query.filter(Repository.repo_type == repo_type)
110 110
111 111 if name_contains:
112 112 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
113 113 query = query.filter(
114 114 Repository.repo_name.ilike(ilike_expression))
115 115 query = query.limit(limit)
116 116
117 117 all_repos = query.all()
118 118 # permission checks are inside this function
119 119 repo_iter = ScmModel().get_repos(all_repos)
120 120 return [
121 121 {
122 122 'id': obj['name'],
123 123 'text': obj['name'],
124 124 'type': 'repo',
125 125 'obj': obj['dbrepo'],
126 126 'url': h.route_path('repo_summary', repo_name=obj['name'])
127 127 }
128 128 for obj in repo_iter]
129 129
130 130 def _get_repo_group_list(self, name_contains=None, limit=20):
131 131 query = RepoGroup.query()\
132 132 .order_by(func.length(RepoGroup.group_name))\
133 133 .order_by(RepoGroup.group_name)
134 134
135 135 if name_contains:
136 136 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
137 137 query = query.filter(
138 138 RepoGroup.group_name.ilike(ilike_expression))
139 139 query = query.limit(limit)
140 140
141 141 all_groups = query.all()
142 142 repo_groups_iter = ScmModel().get_repo_groups(all_groups)
143 143 return [
144 144 {
145 145 'id': obj.group_name,
146 146 'text': obj.group_name,
147 147 'type': 'group',
148 148 'obj': {},
149 149 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name)
150 150 }
151 151 for obj in repo_groups_iter]
152 152
153 153 def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
154 154 if not hash_starts_with or len(hash_starts_with) < 3:
155 155 return []
156 156
157 157 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
158 158
159 159 if len(commit_hashes) != 1:
160 160 return []
161 161
162 162 commit_hash_prefix = commit_hashes[0]
163 163
164 164 searcher = searcher_from_config(self.request.registry.settings)
165 165 result = searcher.search(
166 166 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
167 167 raise_on_exc=False)
168 168
169 169 return [
170 170 {
171 171 'id': entry['commit_id'],
172 172 'text': entry['commit_id'],
173 173 'type': 'commit',
174 174 'obj': {'repo': entry['repository']},
175 'url': h.url('changeset_home',
175 'url': h.route_path('repo_commit',
176 176 repo_name=entry['repository'],
177 revision=entry['commit_id'])
177 commit_id=entry['commit_id'])
178 178 }
179 179 for entry in result['results']]
180 180
181 181 @LoginRequired()
182 182 @view_config(
183 183 route_name='repo_list_data', request_method='GET',
184 184 renderer='json_ext', xhr=True)
185 185 def repo_list_data(self):
186 186 _ = self.request.translate
187 187
188 188 query = self.request.GET.get('query')
189 189 repo_type = self.request.GET.get('repo_type')
190 190 log.debug('generating repo list, query:%s, repo_type:%s',
191 191 query, repo_type)
192 192
193 193 res = []
194 194 repos = self._get_repo_list(query, repo_type=repo_type)
195 195 if repos:
196 196 res.append({
197 197 'text': _('Repositories'),
198 198 'children': repos
199 199 })
200 200
201 201 data = {
202 202 'more': False,
203 203 'results': res
204 204 }
205 205 return data
206 206
207 207 @LoginRequired()
208 208 @view_config(
209 209 route_name='goto_switcher_data', request_method='GET',
210 210 renderer='json_ext', xhr=True)
211 211 def goto_switcher_data(self):
212 212 c = self.load_default_context()
213 213
214 214 _ = self.request.translate
215 215
216 216 query = self.request.GET.get('query')
217 217 log.debug('generating goto switcher list, query %s', query)
218 218
219 219 res = []
220 220 repo_groups = self._get_repo_group_list(query)
221 221 if repo_groups:
222 222 res.append({
223 223 'text': _('Groups'),
224 224 'children': repo_groups
225 225 })
226 226
227 227 repos = self._get_repo_list(query)
228 228 if repos:
229 229 res.append({
230 230 'text': _('Repositories'),
231 231 'children': repos
232 232 })
233 233
234 234 commits = self._get_hash_commit_list(c.auth_user, query)
235 235 if commits:
236 236 unique_repos = {}
237 237 for commit in commits:
238 238 unique_repos.setdefault(commit['obj']['repo'], []
239 239 ).append(commit)
240 240
241 241 for repo in unique_repos:
242 242 res.append({
243 243 'text': _('Commits in %(repo)s') % {'repo': repo},
244 244 'children': unique_repos[repo]
245 245 })
246 246
247 247 data = {
248 248 'more': False,
249 249 'results': res
250 250 }
251 251 return data
252 252
253 253 def _get_groups_and_repos(self, repo_group_id=None):
254 254 # repo groups groups
255 255 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
256 256 _perms = ['group.read', 'group.write', 'group.admin']
257 257 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
258 258 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
259 259 repo_group_list=repo_group_list_acl, admin=False)
260 260
261 261 # repositories
262 262 repo_list = Repository.get_all_repos(group_id=repo_group_id)
263 263 _perms = ['repository.read', 'repository.write', 'repository.admin']
264 264 repo_list_acl = RepoList(repo_list, perm_set=_perms)
265 265 repo_data = RepoModel().get_repos_as_dict(
266 266 repo_list=repo_list_acl, admin=False)
267 267
268 268 return repo_data, repo_group_data
269 269
270 270 @LoginRequired()
271 271 @view_config(
272 272 route_name='home', request_method='GET',
273 273 renderer='rhodecode:templates/index.mako')
274 274 def main_page(self):
275 275 c = self.load_default_context()
276 276 c.repo_group = None
277 277
278 278 repo_data, repo_group_data = self._get_groups_and_repos()
279 279 # json used to render the grids
280 280 c.repos_data = json.dumps(repo_data)
281 281 c.repo_groups_data = json.dumps(repo_group_data)
282 282
283 283 return self._get_template_context(c)
284 284
285 285 @LoginRequired()
286 286 @HasRepoGroupPermissionAnyDecorator(
287 287 'group.read', 'group.write', 'group.admin')
288 288 @view_config(
289 289 route_name='repo_group_home', request_method='GET',
290 290 renderer='rhodecode:templates/index_repo_group.mako')
291 291 @view_config(
292 292 route_name='repo_group_home_slash', request_method='GET',
293 293 renderer='rhodecode:templates/index_repo_group.mako')
294 294 def repo_group_main_page(self):
295 295 c = self.load_default_context()
296 296 c.repo_group = self.request.db_repo_group
297 297 repo_data, repo_group_data = self._get_groups_and_repos(
298 298 c.repo_group.group_id)
299 299
300 300 # json used to render the grids
301 301 c.repos_data = json.dumps(repo_data)
302 302 c.repo_groups_data = json.dumps(repo_group_data)
303 303
304 304 return self._get_template_context(c)
@@ -1,530 +1,529 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 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.tests import (
27 27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 28 no_newline_id_generator)
29 29 from rhodecode.tests.fixture import Fixture
30 30 from rhodecode.lib.auth import check_password
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.model.auth_token import AuthTokenModel
33 33 from rhodecode.model import validators
34 34 from rhodecode.model.db import User, Notification, UserApiKeys
35 35 from rhodecode.model.meta import Session
36 36
37 37 fixture = Fixture()
38 38
39 whitelist_view = ['RepoCommitsView:repo_commit_raw']
40
39 41
40 42 def route_path(name, params=None, **kwargs):
41 43 import urllib
42 44 from rhodecode.apps._base import ADMIN_PREFIX
43 45
44 46 base_url = {
45 47 'login': ADMIN_PREFIX + '/login',
46 48 'logout': ADMIN_PREFIX + '/logout',
47 49 'register': ADMIN_PREFIX + '/register',
48 50 'reset_password':
49 51 ADMIN_PREFIX + '/password_reset',
50 52 'reset_password_confirmation':
51 53 ADMIN_PREFIX + '/password_reset_confirmation',
52 54
53 55 'admin_permissions_application':
54 56 ADMIN_PREFIX + '/permissions/application',
55 57 'admin_permissions_application_update':
56 58 ADMIN_PREFIX + '/permissions/application/update',
57 59
58 60 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59 61
60 62 }[name].format(**kwargs)
61 63
62 64 if params:
63 65 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
64 66 return base_url
65 67
66 68
67 69 @pytest.mark.usefixtures('app')
68 70 class TestLoginController(object):
69 71 destroy_users = set()
70 72
71 73 @classmethod
72 74 def teardown_class(cls):
73 75 fixture.destroy_users(cls.destroy_users)
74 76
75 77 def teardown_method(self, method):
76 78 for n in Notification.query().all():
77 79 Session().delete(n)
78 80
79 81 Session().commit()
80 82 assert Notification.query().all() == []
81 83
82 84 def test_index(self):
83 85 response = self.app.get(route_path('login'))
84 86 assert response.status == '200 OK'
85 87 # Test response...
86 88
87 89 def test_login_admin_ok(self):
88 90 response = self.app.post(route_path('login'),
89 91 {'username': 'test_admin',
90 92 'password': 'test12'})
91 93 assert response.status == '302 Found'
92 94 session = response.get_session_from_response()
93 95 username = session['rhodecode_user'].get('username')
94 96 assert username == 'test_admin'
95 97 response = response.follow()
96 98 response.mustcontain('/%s' % HG_REPO)
97 99
98 100 def test_login_regular_ok(self):
99 101 response = self.app.post(route_path('login'),
100 102 {'username': 'test_regular',
101 103 'password': 'test12'})
102 104
103 105 assert response.status == '302 Found'
104 106 session = response.get_session_from_response()
105 107 username = session['rhodecode_user'].get('username')
106 108 assert username == 'test_regular'
107 109 response = response.follow()
108 110 response.mustcontain('/%s' % HG_REPO)
109 111
110 112 def test_login_ok_came_from(self):
111 113 test_came_from = '/_admin/users?branch=stable'
112 114 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
113 115 response = self.app.post(
114 116 _url, {'username': 'test_admin', 'password': 'test12'})
115 117 assert response.status == '302 Found'
116 118 assert 'branch=stable' in response.location
117 119 response = response.follow()
118 120
119 121 assert response.status == '200 OK'
120 122 response.mustcontain('Users administration')
121 123
122 124 def test_redirect_to_login_with_get_args(self):
123 125 with fixture.anon_access(False):
124 126 kwargs = {'branch': 'stable'}
125 127 response = self.app.get(
126 128 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs))
127 129 assert response.status == '302 Found'
128 130
129 131 response_query = urlparse.parse_qsl(response.location)
130 132 assert 'branch=stable' in response_query[0][1]
131 133
132 134 def test_login_form_with_get_args(self):
133 135 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
134 136 response = self.app.get(_url)
135 137 assert 'branch%3Dstable' in response.form.action
136 138
137 139 @pytest.mark.parametrize("url_came_from", [
138 140 'data:text/html,<script>window.alert("xss")</script>',
139 141 'mailto:test@rhodecode.org',
140 142 'file:///etc/passwd',
141 143 'ftp://some.ftp.server',
142 144 'http://other.domain',
143 145 '/\r\nX-Forwarded-Host: http://example.org',
144 146 ], ids=no_newline_id_generator)
145 147 def test_login_bad_came_froms(self, url_came_from):
146 148 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
147 149 response = self.app.post(
148 150 _url,
149 151 {'username': 'test_admin', 'password': 'test12'})
150 152 assert response.status == '302 Found'
151 153 response = response.follow()
152 154 assert response.status == '200 OK'
153 155 assert response.request.path == '/'
154 156
155 157 def test_login_short_password(self):
156 158 response = self.app.post(route_path('login'),
157 159 {'username': 'test_admin',
158 160 'password': 'as'})
159 161 assert response.status == '200 OK'
160 162
161 163 response.mustcontain('Enter 3 characters or more')
162 164
163 165 def test_login_wrong_non_ascii_password(self, user_regular):
164 166 response = self.app.post(
165 167 route_path('login'),
166 168 {'username': user_regular.username,
167 169 'password': u'invalid-non-asci\xe4'.encode('utf8')})
168 170
169 171 response.mustcontain('invalid user name')
170 172 response.mustcontain('invalid password')
171 173
172 174 def test_login_with_non_ascii_password(self, user_util):
173 175 password = u'valid-non-ascii\xe4'
174 176 user = user_util.create_user(password=password)
175 177 response = self.app.post(
176 178 route_path('login'),
177 179 {'username': user.username,
178 180 'password': password.encode('utf-8')})
179 181 assert response.status_code == 302
180 182
181 183 def test_login_wrong_username_password(self):
182 184 response = self.app.post(route_path('login'),
183 185 {'username': 'error',
184 186 'password': 'test12'})
185 187
186 188 response.mustcontain('invalid user name')
187 189 response.mustcontain('invalid password')
188 190
189 191 def test_login_admin_ok_password_migration(self, real_crypto_backend):
190 192 from rhodecode.lib import auth
191 193
192 194 # create new user, with sha256 password
193 195 temp_user = 'test_admin_sha256'
194 196 user = fixture.create_user(temp_user)
195 197 user.password = auth._RhodeCodeCryptoSha256().hash_create(
196 198 b'test123')
197 199 Session().add(user)
198 200 Session().commit()
199 201 self.destroy_users.add(temp_user)
200 202 response = self.app.post(route_path('login'),
201 203 {'username': temp_user,
202 204 'password': 'test123'})
203 205
204 206 assert response.status == '302 Found'
205 207 session = response.get_session_from_response()
206 208 username = session['rhodecode_user'].get('username')
207 209 assert username == temp_user
208 210 response = response.follow()
209 211 response.mustcontain('/%s' % HG_REPO)
210 212
211 213 # new password should be bcrypted, after log-in and transfer
212 214 user = User.get_by_username(temp_user)
213 215 assert user.password.startswith('$')
214 216
215 217 # REGISTRATIONS
216 218 def test_register(self):
217 219 response = self.app.get(route_path('register'))
218 220 response.mustcontain('Create an Account')
219 221
220 222 def test_register_err_same_username(self):
221 223 uname = 'test_admin'
222 224 response = self.app.post(
223 225 route_path('register'),
224 226 {
225 227 'username': uname,
226 228 'password': 'test12',
227 229 'password_confirmation': 'test12',
228 230 'email': 'goodmail@domain.com',
229 231 'firstname': 'test',
230 232 'lastname': 'test'
231 233 }
232 234 )
233 235
234 236 assertr = response.assert_response()
235 237 msg = validators.ValidUsername()._messages['username_exists']
236 238 msg = msg % {'username': uname}
237 239 assertr.element_contains('#username+.error-message', msg)
238 240
239 241 def test_register_err_same_email(self):
240 242 response = self.app.post(
241 243 route_path('register'),
242 244 {
243 245 'username': 'test_admin_0',
244 246 'password': 'test12',
245 247 'password_confirmation': 'test12',
246 248 'email': 'test_admin@mail.com',
247 249 'firstname': 'test',
248 250 'lastname': 'test'
249 251 }
250 252 )
251 253
252 254 assertr = response.assert_response()
253 255 msg = validators.UniqSystemEmail()()._messages['email_taken']
254 256 assertr.element_contains('#email+.error-message', msg)
255 257
256 258 def test_register_err_same_email_case_sensitive(self):
257 259 response = self.app.post(
258 260 route_path('register'),
259 261 {
260 262 'username': 'test_admin_1',
261 263 'password': 'test12',
262 264 'password_confirmation': 'test12',
263 265 'email': 'TesT_Admin@mail.COM',
264 266 'firstname': 'test',
265 267 'lastname': 'test'
266 268 }
267 269 )
268 270 assertr = response.assert_response()
269 271 msg = validators.UniqSystemEmail()()._messages['email_taken']
270 272 assertr.element_contains('#email+.error-message', msg)
271 273
272 274 def test_register_err_wrong_data(self):
273 275 response = self.app.post(
274 276 route_path('register'),
275 277 {
276 278 'username': 'xs',
277 279 'password': 'test',
278 280 'password_confirmation': 'test',
279 281 'email': 'goodmailm',
280 282 'firstname': 'test',
281 283 'lastname': 'test'
282 284 }
283 285 )
284 286 assert response.status == '200 OK'
285 287 response.mustcontain('An email address must contain a single @')
286 288 response.mustcontain('Enter a value 6 characters long or more')
287 289
288 290 def test_register_err_username(self):
289 291 response = self.app.post(
290 292 route_path('register'),
291 293 {
292 294 'username': 'error user',
293 295 'password': 'test12',
294 296 'password_confirmation': 'test12',
295 297 'email': 'goodmailm',
296 298 'firstname': 'test',
297 299 'lastname': 'test'
298 300 }
299 301 )
300 302
301 303 response.mustcontain('An email address must contain a single @')
302 304 response.mustcontain(
303 305 'Username may only contain '
304 306 'alphanumeric characters underscores, '
305 307 'periods or dashes and must begin with '
306 308 'alphanumeric character')
307 309
308 310 def test_register_err_case_sensitive(self):
309 311 usr = 'Test_Admin'
310 312 response = self.app.post(
311 313 route_path('register'),
312 314 {
313 315 'username': usr,
314 316 'password': 'test12',
315 317 'password_confirmation': 'test12',
316 318 'email': 'goodmailm',
317 319 'firstname': 'test',
318 320 'lastname': 'test'
319 321 }
320 322 )
321 323
322 324 assertr = response.assert_response()
323 325 msg = validators.ValidUsername()._messages['username_exists']
324 326 msg = msg % {'username': usr}
325 327 assertr.element_contains('#username+.error-message', msg)
326 328
327 329 def test_register_special_chars(self):
328 330 response = self.app.post(
329 331 route_path('register'),
330 332 {
331 333 'username': 'xxxaxn',
332 334 'password': 'ąćźżąśśśś',
333 335 'password_confirmation': 'ąćźżąśśśś',
334 336 'email': 'goodmailm@test.plx',
335 337 'firstname': 'test',
336 338 'lastname': 'test'
337 339 }
338 340 )
339 341
340 342 msg = validators.ValidPassword()._messages['invalid_password']
341 343 response.mustcontain(msg)
342 344
343 345 def test_register_password_mismatch(self):
344 346 response = self.app.post(
345 347 route_path('register'),
346 348 {
347 349 'username': 'xs',
348 350 'password': '123qwe',
349 351 'password_confirmation': 'qwe123',
350 352 'email': 'goodmailm@test.plxa',
351 353 'firstname': 'test',
352 354 'lastname': 'test'
353 355 }
354 356 )
355 357 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
356 358 response.mustcontain(msg)
357 359
358 360 def test_register_ok(self):
359 361 username = 'test_regular4'
360 362 password = 'qweqwe'
361 363 email = 'marcin@test.com'
362 364 name = 'testname'
363 365 lastname = 'testlastname'
364 366
365 367 response = self.app.post(
366 368 route_path('register'),
367 369 {
368 370 'username': username,
369 371 'password': password,
370 372 'password_confirmation': password,
371 373 'email': email,
372 374 'firstname': name,
373 375 'lastname': lastname,
374 376 'admin': True
375 377 }
376 378 ) # This should be overriden
377 379 assert response.status == '302 Found'
378 380 assert_session_flash(
379 381 response, 'You have successfully registered with RhodeCode')
380 382
381 383 ret = Session().query(User).filter(
382 384 User.username == 'test_regular4').one()
383 385 assert ret.username == username
384 386 assert check_password(password, ret.password)
385 387 assert ret.email == email
386 388 assert ret.name == name
387 389 assert ret.lastname == lastname
388 390 assert ret.auth_tokens is not None
389 391 assert not ret.admin
390 392
391 393 def test_forgot_password_wrong_mail(self):
392 394 bad_email = 'marcin@wrongmail.org'
393 395 response = self.app.post(
394 396 route_path('reset_password'), {'email': bad_email, }
395 397 )
396 398 assert_session_flash(response,
397 399 'If such email exists, a password reset link was sent to it.')
398 400
399 401 def test_forgot_password(self, user_util):
400 402 response = self.app.get(route_path('reset_password'))
401 403 assert response.status == '200 OK'
402 404
403 405 user = user_util.create_user()
404 406 user_id = user.user_id
405 407 email = user.email
406 408
407 409 response = self.app.post(route_path('reset_password'), {'email': email, })
408 410
409 411 assert_session_flash(response,
410 412 'If such email exists, a password reset link was sent to it.')
411 413
412 414 # BAD KEY
413 415 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
414 416 response = self.app.get(confirm_url)
415 417 assert response.status == '302 Found'
416 418 assert response.location.endswith(route_path('reset_password'))
417 419 assert_session_flash(response, 'Given reset token is invalid')
418 420
419 421 response.follow() # cleanup flash
420 422
421 423 # GOOD KEY
422 424 key = UserApiKeys.query()\
423 425 .filter(UserApiKeys.user_id == user_id)\
424 426 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
425 427 .first()
426 428
427 429 assert key
428 430
429 431 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
430 432 response = self.app.get(confirm_url)
431 433 assert response.status == '302 Found'
432 434 assert response.location.endswith(route_path('login'))
433 435
434 436 assert_session_flash(
435 437 response,
436 438 'Your password reset was successful, '
437 439 'a new password has been sent to your email')
438 440
439 441 response.follow()
440 442
441 443 def _get_api_whitelist(self, values=None):
442 444 config = {'api_access_controllers_whitelist': values or []}
443 445 return config
444 446
445 447 @pytest.mark.parametrize("test_name, auth_token", [
446 448 ('none', None),
447 449 ('empty_string', ''),
448 450 ('fake_number', '123456'),
449 451 ('proper_auth_token', None)
450 452 ])
451 453 def test_access_not_whitelisted_page_via_auth_token(
452 454 self, test_name, auth_token, user_admin):
453 455
454 456 whitelist = self._get_api_whitelist([])
455 457 with mock.patch.dict('rhodecode.CONFIG', whitelist):
456 458 assert [] == whitelist['api_access_controllers_whitelist']
457 459 if test_name == 'proper_auth_token':
458 460 # use builtin if api_key is None
459 461 auth_token = user_admin.api_key
460 462
461 463 with fixture.anon_access(False):
462 464 self.app.get(
463 465 route_path('repo_commit_raw',
464 466 repo_name=HG_REPO, commit_id='tip',
465 467 params=dict(api_key=auth_token)),
466 468 status=302)
467 469
468 470 @pytest.mark.parametrize("test_name, auth_token, code", [
469 471 ('none', None, 302),
470 472 ('empty_string', '', 302),
471 473 ('fake_number', '123456', 302),
472 474 ('proper_auth_token', None, 200)
473 475 ])
474 476 def test_access_whitelisted_page_via_auth_token(
475 477 self, test_name, auth_token, code, user_admin):
476 478
477 whitelist_entry = ['ChangesetController:changeset_raw']
478 whitelist = self._get_api_whitelist(whitelist_entry)
479 whitelist = self._get_api_whitelist(whitelist_view)
479 480
480 481 with mock.patch.dict('rhodecode.CONFIG', whitelist):
481 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
482 assert whitelist_view == whitelist['api_access_controllers_whitelist']
482 483
483 484 if test_name == 'proper_auth_token':
484 485 auth_token = user_admin.api_key
485 486 assert auth_token
486 487
487 488 with fixture.anon_access(False):
488 489 self.app.get(
489 490 route_path('repo_commit_raw',
490 491 repo_name=HG_REPO, commit_id='tip',
491 492 params=dict(api_key=auth_token)),
492 493 status=code)
493 494
494 495 def test_access_page_via_extra_auth_token(self):
495 whitelist = self._get_api_whitelist(
496 ['ChangesetController:changeset_raw'])
496 whitelist = self._get_api_whitelist(whitelist_view)
497 497 with mock.patch.dict('rhodecode.CONFIG', whitelist):
498 assert ['ChangesetController:changeset_raw'] == \
498 assert whitelist_view == \
499 499 whitelist['api_access_controllers_whitelist']
500 500
501 501 new_auth_token = AuthTokenModel().create(
502 502 TEST_USER_ADMIN_LOGIN, 'test')
503 503 Session().commit()
504 504 with fixture.anon_access(False):
505 505 self.app.get(
506 506 route_path('repo_commit_raw',
507 507 repo_name=HG_REPO, commit_id='tip',
508 508 params=dict(api_key=new_auth_token.api_key)),
509 509 status=200)
510 510
511 511 def test_access_page_via_expired_auth_token(self):
512 whitelist = self._get_api_whitelist(
513 ['ChangesetController:changeset_raw'])
512 whitelist = self._get_api_whitelist(whitelist_view)
514 513 with mock.patch.dict('rhodecode.CONFIG', whitelist):
515 assert ['ChangesetController:changeset_raw'] == \
514 assert whitelist_view == \
516 515 whitelist['api_access_controllers_whitelist']
517 516
518 517 new_auth_token = AuthTokenModel().create(
519 518 TEST_USER_ADMIN_LOGIN, 'test')
520 519 Session().commit()
521 520 # patch the api key and make it expired
522 521 new_auth_token.expires = 0
523 522 Session().add(new_auth_token)
524 523 Session().commit()
525 524 with fixture.anon_access(False):
526 525 self.app.get(
527 526 route_path('repo_commit_raw',
528 527 repo_name=HG_REPO, commit_id='tip',
529 528 params=dict(api_key=new_auth_token.api_key)),
530 529 status=302)
@@ -1,277 +1,304 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24
25 25 # Summary
26 26 # NOTE(marcink): one additional route is defined in very bottom, catch
27 27 # all pattern
28 28 config.add_route(
29 29 name='repo_summary_explicit',
30 30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 31 config.add_route(
32 32 name='repo_summary_commits',
33 33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
34 34
35 35 # repo commits
36
36 37 config.add_route(
37 38 name='repo_commit',
38 39 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
39 40
41 config.add_route(
42 name='repo_commit_children',
43 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
44
45 config.add_route(
46 name='repo_commit_parents',
47 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
48
49 # still working url for backward compat.
50 config.add_route(
51 name='repo_commit_raw_deprecated',
52 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
53
54 config.add_route(
55 name='repo_commit_raw',
56 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
57
58 config.add_route(
59 name='repo_commit_patch',
60 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
61
62 config.add_route(
63 name='repo_commit_download',
64 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
65
66 config.add_route(
67 name='repo_commit_data',
68 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
69
70 config.add_route(
71 name='repo_commit_comment_create',
72 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
73
74 config.add_route(
75 name='repo_commit_comment_preview',
76 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
77
78 config.add_route(
79 name='repo_commit_comment_delete',
80 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
81
40 82 # repo files
41 83 config.add_route(
42 84 name='repo_archivefile',
43 85 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
44 86
45 87 config.add_route(
46 88 name='repo_files_diff',
47 89 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
48 90 config.add_route( # legacy route to make old links work
49 91 name='repo_files_diff_2way_redirect',
50 92 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
51 93
52 94 config.add_route(
53 95 name='repo_files',
54 96 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
55 97 config.add_route(
56 98 name='repo_files:default_path',
57 99 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
58 100 config.add_route(
59 101 name='repo_files:default_commit',
60 102 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
61 103
62 104 config.add_route(
63 105 name='repo_files:rendered',
64 106 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
65 107
66 108 config.add_route(
67 109 name='repo_files:annotated',
68 110 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
69 111 config.add_route(
70 112 name='repo_files:annotated_previous',
71 113 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
72 114
73 115 config.add_route(
74 116 name='repo_nodetree_full',
75 117 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
76 118 config.add_route(
77 119 name='repo_nodetree_full:default_path',
78 120 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
79 121
80 122 config.add_route(
81 123 name='repo_files_nodelist',
82 124 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
83 125
84 126 config.add_route(
85 127 name='repo_file_raw',
86 128 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
87 129
88 130 config.add_route(
89 131 name='repo_file_download',
90 132 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
91 133 config.add_route( # backward compat to keep old links working
92 134 name='repo_file_download:legacy',
93 135 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
94 136 repo_route=True)
95 137
96 138 config.add_route(
97 139 name='repo_file_history',
98 140 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
99 141
100 142 config.add_route(
101 143 name='repo_file_authors',
102 144 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
103 145
104 146 config.add_route(
105 147 name='repo_files_remove_file',
106 148 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
107 149 repo_route=True)
108 150 config.add_route(
109 151 name='repo_files_delete_file',
110 152 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
111 153 repo_route=True)
112 154 config.add_route(
113 155 name='repo_files_edit_file',
114 156 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
115 157 repo_route=True)
116 158 config.add_route(
117 159 name='repo_files_update_file',
118 160 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
119 161 repo_route=True)
120 162 config.add_route(
121 163 name='repo_files_add_file',
122 164 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
123 165 repo_route=True)
124 166 config.add_route(
125 167 name='repo_files_create_file',
126 168 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
127 169 repo_route=True)
128 170
129 171 # refs data
130 172 config.add_route(
131 173 name='repo_refs_data',
132 174 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
133 175
134 176 config.add_route(
135 177 name='repo_refs_changelog_data',
136 178 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
137 179
138 180 config.add_route(
139 181 name='repo_stats',
140 182 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
141 183
142 184 # Changelog
143 185 config.add_route(
144 186 name='repo_changelog',
145 187 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
146 188 config.add_route(
147 189 name='repo_changelog_file',
148 190 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
149 191 config.add_route(
150 192 name='repo_changelog_elements',
151 193 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
152 194
153 195 # Tags
154 196 config.add_route(
155 197 name='tags_home',
156 198 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
157 199
158 200 # Branches
159 201 config.add_route(
160 202 name='branches_home',
161 203 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
162 204
163 205 config.add_route(
164 206 name='bookmarks_home',
165 207 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
166 208
167 209 # Pull Requests
168 210 config.add_route(
169 211 name='pullrequest_show',
170 212 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
171 213 repo_route=True)
172 214
173 215 config.add_route(
174 216 name='pullrequest_show_all',
175 217 pattern='/{repo_name:.*?[^/]}/pull-request',
176 218 repo_route=True, repo_accepted_types=['hg', 'git'])
177 219
178 220 config.add_route(
179 221 name='pullrequest_show_all_data',
180 222 pattern='/{repo_name:.*?[^/]}/pull-request-data',
181 223 repo_route=True, repo_accepted_types=['hg', 'git'])
182 224
183 # commits aka changesets
184 # TODO(dan): handle default landing revision ?
185 config.add_route(
186 name='changeset_home',
187 pattern='/{repo_name:.*?[^/]}/changeset/{revision}',
188 repo_route=True)
189 config.add_route(
190 name='changeset_children',
191 pattern='/{repo_name:.*?[^/]}/changeset_children/{revision}',
192 repo_route=True)
193 config.add_route(
194 name='changeset_parents',
195 pattern='/{repo_name:.*?[^/]}/changeset_parents/{revision}',
196 repo_route=True)
197
198 225 # Settings
199 226 config.add_route(
200 227 name='edit_repo',
201 228 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
202 229
203 230 # Settings advanced
204 231 config.add_route(
205 232 name='edit_repo_advanced',
206 233 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
207 234 config.add_route(
208 235 name='edit_repo_advanced_delete',
209 236 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
210 237 config.add_route(
211 238 name='edit_repo_advanced_locking',
212 239 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
213 240 config.add_route(
214 241 name='edit_repo_advanced_journal',
215 242 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
216 243 config.add_route(
217 244 name='edit_repo_advanced_fork',
218 245 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
219 246
220 247 # Caches
221 248 config.add_route(
222 249 name='edit_repo_caches',
223 250 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
224 251
225 252 # Permissions
226 253 config.add_route(
227 254 name='edit_repo_perms',
228 255 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
229 256
230 257 # Repo Review Rules
231 258 config.add_route(
232 259 name='repo_reviewers',
233 260 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
234 261
235 262 config.add_route(
236 263 name='repo_default_reviewers_data',
237 264 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
238 265
239 266 # Maintenance
240 267 config.add_route(
241 268 name='repo_maintenance',
242 269 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
243 270
244 271 config.add_route(
245 272 name='repo_maintenance_execute',
246 273 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
247 274
248 275 # Strip
249 276 config.add_route(
250 277 name='strip',
251 278 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
252 279
253 280 config.add_route(
254 281 name='strip_check',
255 282 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
256 283
257 284 config.add_route(
258 285 name='strip_execute',
259 286 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
260 287
261 288 # ATOM/RSS Feed
262 289 config.add_route(
263 290 name='rss_feed_home',
264 291 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
265 292
266 293 config.add_route(
267 294 name='atom_feed_home',
268 295 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
269 296
270 297 # NOTE(marcink): needs to be at the end for catch-all
271 298 add_route_with_slash(
272 299 config,
273 300 name='repo_summary',
274 301 pattern='/{repo_name:.*?[^/]}', repo_route=True)
275 302
276 303 # Scan module for configuration decorators.
277 304 config.scan()
@@ -1,288 +1,313 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 from pylons.i18n import ungettext
22 21 import pytest
23 22
24 from rhodecode.tests import *
23 from rhodecode.tests import TestController
24
25 25 from rhodecode.model.db import (
26 26 ChangesetComment, Notification, UserNotification)
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.lib import helpers as h
29 29
30 30
31 def route_path(name, params=None, **kwargs):
32 import urllib
33
34 base_url = {
35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
39 }[name].format(**kwargs)
40
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 return base_url
44
45
31 46 @pytest.mark.backends("git", "hg", "svn")
32 class TestCommitCommentsController(TestController):
47 class TestRepoCommitCommentsView(TestController):
33 48
34 49 @pytest.fixture(autouse=True)
35 50 def prepare(self, request, pylonsapp):
36 51 for x in ChangesetComment.query().all():
37 52 Session().delete(x)
38 53 Session().commit()
39 54
40 55 for x in Notification.query().all():
41 56 Session().delete(x)
42 57 Session().commit()
43 58
44 59 request.addfinalizer(self.cleanup)
45 60
46 61 def cleanup(self):
47 62 for x in ChangesetComment.query().all():
48 63 Session().delete(x)
49 64 Session().commit()
50 65
51 66 for x in Notification.query().all():
52 67 Session().delete(x)
53 68 Session().commit()
54 69
55 70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
56 71 def test_create(self, comment_type, backend):
57 72 self.log_user()
58 73 commit = backend.repo.get_commit('300')
59 74 commit_id = commit.raw_id
60 75 text = u'CommentOnCommit'
61 76
62 77 params = {'text': text, 'csrf_token': self.csrf_token,
63 78 'comment_type': comment_type}
64 79 self.app.post(
65 url(controller='changeset', action='comment',
66 repo_name=backend.repo_name, revision=commit_id), params=params)
80 route_path('repo_commit_comment_create',
81 repo_name=backend.repo_name, commit_id=commit_id),
82 params=params)
67 83
68 84 response = self.app.get(
69 url(controller='changeset', action='index',
70 repo_name=backend.repo_name, revision=commit_id))
85 route_path('repo_commit',
86 repo_name=backend.repo_name, commit_id=commit_id))
71 87
72 88 # test DB
73 89 assert ChangesetComment.query().count() == 1
74 90 assert_comment_links(response, ChangesetComment.query().count(), 0)
75 91
76 92 assert Notification.query().count() == 1
77 93 assert ChangesetComment.query().count() == 1
78 94
79 95 notification = Notification.query().all()[0]
80 96
81 97 comment_id = ChangesetComment.query().first().comment_id
82 98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
83 99
84 100 sbj = 'left {0} on commit `{1}` in the {2} repository'.format(
85 101 comment_type, h.show_id(commit), backend.repo_name)
86 102 assert sbj in notification.subject
87 103
88 104 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
89 105 backend.repo_name, commit_id, comment_id))
90 106 assert lnk in notification.body
91 107
92 108 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
93 109 def test_create_inline(self, comment_type, backend):
94 110 self.log_user()
95 111 commit = backend.repo.get_commit('300')
96 112 commit_id = commit.raw_id
97 113 text = u'CommentOnCommit'
98 114 f_path = 'vcs/web/simplevcs/views/repository.py'
99 115 line = 'n1'
100 116
101 117 params = {'text': text, 'f_path': f_path, 'line': line,
102 118 'comment_type': comment_type,
103 119 'csrf_token': self.csrf_token}
104 120
105 121 self.app.post(
106 url(controller='changeset', action='comment',
107 repo_name=backend.repo_name, revision=commit_id), params=params)
122 route_path('repo_commit_comment_create',
123 repo_name=backend.repo_name, commit_id=commit_id),
124 params=params)
108 125
109 126 response = self.app.get(
110 url(controller='changeset', action='index',
111 repo_name=backend.repo_name, revision=commit_id))
127 route_path('repo_commit',
128 repo_name=backend.repo_name, commit_id=commit_id))
112 129
113 130 # test DB
114 131 assert ChangesetComment.query().count() == 1
115 132 assert_comment_links(response, 0, ChangesetComment.query().count())
116 133
117 134 if backend.alias == 'svn':
118 135 response.mustcontain(
119 136 '''data-f-path="vcs/commands/summary.py" '''
120 137 '''id="a_c--ad05457a43f8"'''
121 138 )
122 139 else:
123 140 response.mustcontain(
124 141 '''data-f-path="vcs/backends/hg.py" '''
125 142 '''id="a_c--9c390eb52cd6"'''
126 143 )
127 144
128 145 assert Notification.query().count() == 1
129 146 assert ChangesetComment.query().count() == 1
130 147
131 148 notification = Notification.query().all()[0]
132 149 comment = ChangesetComment.query().first()
133 150 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
134 151
135 152 assert comment.revision == commit_id
136 153 sbj = 'left {comment_type} on commit `{commit}` ' \
137 154 '(file: `{f_path}`) in the {repo} repository'.format(
138 155 commit=h.show_id(commit),
139 156 f_path=f_path, line=line, repo=backend.repo_name,
140 157 comment_type=comment_type)
141 158 assert sbj in notification.subject
142 159
143 160 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
144 161 backend.repo_name, commit_id, comment.comment_id))
145 162 assert lnk in notification.body
146 163 assert 'on line n1' in notification.body
147 164
148 165 def test_create_with_mention(self, backend):
149 166 self.log_user()
150 167
151 168 commit_id = backend.repo.get_commit('300').raw_id
152 169 text = u'@test_regular check CommentOnCommit'
153 170
154 171 params = {'text': text, 'csrf_token': self.csrf_token}
155 172 self.app.post(
156 url(controller='changeset', action='comment',
157 repo_name=backend.repo_name, revision=commit_id), params=params)
173 route_path('repo_commit_comment_create',
174 repo_name=backend.repo_name, commit_id=commit_id),
175 params=params)
158 176
159 177 response = self.app.get(
160 url(controller='changeset', action='index',
161 repo_name=backend.repo_name, revision=commit_id))
178 route_path('repo_commit',
179 repo_name=backend.repo_name, commit_id=commit_id))
162 180 # test DB
163 181 assert ChangesetComment.query().count() == 1
164 182 assert_comment_links(response, ChangesetComment.query().count(), 0)
165 183
166 184 notification = Notification.query().one()
167 185
168 186 assert len(notification.recipients) == 2
169 187 users = [x.username for x in notification.recipients]
170 188
171 189 # test_regular gets notification by @mention
172 190 assert sorted(users) == [u'test_admin', u'test_regular']
173 191
174 192 def test_create_with_status_change(self, backend):
175 193 self.log_user()
176 194 commit = backend.repo.get_commit('300')
177 195 commit_id = commit.raw_id
178 196 text = u'CommentOnCommit'
179 197 f_path = 'vcs/web/simplevcs/views/repository.py'
180 198 line = 'n1'
181 199
182 200 params = {'text': text, 'changeset_status': 'approved',
183 201 'csrf_token': self.csrf_token}
184 202
185 203 self.app.post(
186 url(controller='changeset', action='comment',
187 repo_name=backend.repo_name, revision=commit_id), params=params)
204 route_path(
205 'repo_commit_comment_create',
206 repo_name=backend.repo_name, commit_id=commit_id),
207 params=params)
188 208
189 209 response = self.app.get(
190 url(controller='changeset', action='index',
191 repo_name=backend.repo_name, revision=commit_id))
210 route_path('repo_commit',
211 repo_name=backend.repo_name, commit_id=commit_id))
192 212
193 213 # test DB
194 214 assert ChangesetComment.query().count() == 1
195 215 assert_comment_links(response, ChangesetComment.query().count(), 0)
196 216
197 217 assert Notification.query().count() == 1
198 218 assert ChangesetComment.query().count() == 1
199 219
200 220 notification = Notification.query().all()[0]
201 221
202 222 comment_id = ChangesetComment.query().first().comment_id
203 223 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
204 224
205 225 sbj = 'left note on commit `{0}` (status: Approved) ' \
206 226 'in the {1} repository'.format(
207 227 h.show_id(commit), backend.repo_name)
208 228 assert sbj in notification.subject
209 229
210 230 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
211 231 backend.repo_name, commit_id, comment_id))
212 232 assert lnk in notification.body
213 233
214 234 def test_delete(self, backend):
215 235 self.log_user()
216 236 commit_id = backend.repo.get_commit('300').raw_id
217 237 text = u'CommentOnCommit'
218 238
219 239 params = {'text': text, 'csrf_token': self.csrf_token}
220 240 self.app.post(
221 url(
222 controller='changeset', action='comment',
223 repo_name=backend.repo_name, revision=commit_id),
241 route_path(
242 'repo_commit_comment_create',
243 repo_name=backend.repo_name, commit_id=commit_id),
224 244 params=params)
225 245
226 246 comments = ChangesetComment.query().all()
227 247 assert len(comments) == 1
228 248 comment_id = comments[0].comment_id
229 249
230 250 self.app.post(
231 url(controller='changeset', action='delete_comment',
232 repo_name=backend.repo_name, comment_id=comment_id),
233 params={'_method': 'delete', 'csrf_token': self.csrf_token})
251 route_path('repo_commit_comment_delete',
252 repo_name=backend.repo_name,
253 commit_id=commit_id,
254 comment_id=comment_id),
255 params={'csrf_token': self.csrf_token})
234 256
235 257 comments = ChangesetComment.query().all()
236 258 assert len(comments) == 0
237 259
238 260 response = self.app.get(
239 url(controller='changeset', action='index',
240 repo_name=backend.repo_name, revision=commit_id))
261 route_path('repo_commit',
262 repo_name=backend.repo_name, commit_id=commit_id))
241 263 assert_comment_links(response, 0, 0)
242 264
243 265 @pytest.mark.parametrize('renderer, input, output', [
244 266 ('rst', 'plain text', '<p>plain text</p>'),
245 267 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
246 268 ('rst', '*italics*', '<em>italics</em>'),
247 269 ('rst', '**bold**', '<strong>bold</strong>'),
248 270 ('markdown', 'plain text', '<p>plain text</p>'),
249 271 ('markdown', '# header', '<h1>header</h1>'),
250 272 ('markdown', '*italics*', '<em>italics</em>'),
251 273 ('markdown', '**bold**', '<strong>bold</strong>'),
252 274 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
253 275 'md-header', 'md-italics', 'md-bold', ])
254 def test_preview(self, renderer, input, output, backend):
276 def test_preview(self, renderer, input, output, backend, xhr_header):
255 277 self.log_user()
256 278 params = {
257 279 'renderer': renderer,
258 280 'text': input,
259 281 'csrf_token': self.csrf_token
260 282 }
261 environ = {
262 'HTTP_X_PARTIAL_XHR': 'true'
263 }
283 commit_id = '0' * 16 # fake this for tests
264 284 response = self.app.post(
265 url(controller='changeset',
266 action='preview_comment',
267 repo_name=backend.repo_name),
285 route_path('repo_commit_comment_preview',
286 repo_name=backend.repo_name, commit_id=commit_id,),
268 287 params=params,
269 extra_environ=environ)
288 extra_environ=xhr_header)
270 289
271 290 response.mustcontain(output)
272 291
273 292
274 293 def assert_comment_links(response, comments, inline_comments):
275 comments_text = ungettext("%d Commit comment",
276 "%d Commit comments", comments) % comments
294 if comments == 1:
295 comments_text = "%d Commit comment" % comments
296 else:
297 comments_text = "%d Commit comments" % comments
298
299 if inline_comments == 1:
300 inline_comments_text = "%d Inline Comment" % inline_comments
301 else:
302 inline_comments_text = "%d Inline Comments" % inline_comments
303
277 304 if comments:
278 305 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
279 306 else:
280 307 response.mustcontain(comments_text)
281 308
282 inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
283 inline_comments) % inline_comments
284 309 if inline_comments:
285 310 response.mustcontain(
286 311 'id="inline-comments-counter">%s</' % inline_comments_text)
287 312 else:
288 313 response.mustcontain(inline_comments_text)
@@ -1,301 +1,318 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 import pytest
22 22
23 23 from rhodecode.lib.helpers import _shorten_commit_id
24 from rhodecode.tests import url
24
25
26 def route_path(name, params=None, **kwargs):
27 import urllib
28
29 base_url = {
30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 }[name].format(**kwargs)
39
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 return base_url
25 43
26 44
27 45 @pytest.mark.usefixtures("app")
28 class TestChangesetController(object):
46 class TestRepoCommitView(object):
29 47
30 def test_index(self, backend):
48 def test_show_commit(self, backend):
31 49 commit_id = self.commit_id[backend.alias]
32 response = self.app.get(url(
33 controller='changeset', action='index',
34 repo_name=backend.repo_name, revision=commit_id))
50 response = self.app.get(route_path(
51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
35 52 response.mustcontain('Added a symlink')
36 53 response.mustcontain(commit_id)
37 54 response.mustcontain('No newline at end of file')
38 55
39 def test_index_raw(self, backend):
56 def test_show_raw(self, backend):
40 57 commit_id = self.commit_id[backend.alias]
41 response = self.app.get(url(
42 controller='changeset', action='changeset_raw',
43 repo_name=backend.repo_name, revision=commit_id))
58 response = self.app.get(route_path(
59 'repo_commit_raw',
60 repo_name=backend.repo_name, commit_id=commit_id))
44 61 assert response.body == self.diffs[backend.alias]
45 62
46 def test_index_raw_patch(self, backend):
47 response = self.app.get(url(
48 controller='changeset', action='changeset_patch',
49 repo_name=backend.repo_name,
50 revision=self.commit_id[backend.alias]))
63 def test_show_raw_patch(self, backend):
64 response = self.app.get(route_path(
65 'repo_commit_patch', repo_name=backend.repo_name,
66 commit_id=self.commit_id[backend.alias]))
51 67 assert response.body == self.patches[backend.alias]
52 68
53 def test_index_changeset_download(self, backend):
54 response = self.app.get(url(
55 controller='changeset', action='changeset_download',
69 def test_commit_download(self, backend):
70 response = self.app.get(route_path(
71 'repo_commit_download',
56 72 repo_name=backend.repo_name,
57 revision=self.commit_id[backend.alias]))
73 commit_id=self.commit_id[backend.alias]))
58 74 assert response.body == self.diffs[backend.alias]
59 75
60 76 def test_single_commit_page_different_ops(self, backend):
61 77 commit_id = {
62 78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
63 79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
64 80 'svn': '337',
65 81 }
66 82 commit_id = commit_id[backend.alias]
67 response = self.app.get(url(
68 controller='changeset', action='index',
69 repo_name=backend.repo_name, revision=commit_id))
83 response = self.app.get(route_path(
84 'repo_commit',
85 repo_name=backend.repo_name, commit_id=commit_id))
70 86
71 87 response.mustcontain(_shorten_commit_id(commit_id))
72 88 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
73 89
74 90 # files op files
75 91 response.mustcontain('File no longer present at commit: %s' %
76 92 _shorten_commit_id(commit_id))
77 93
78 94 # svn uses a different filename
79 95 if backend.alias == 'svn':
80 96 response.mustcontain('new file 10644')
81 97 else:
82 98 response.mustcontain('new file 100644')
83 99 response.mustcontain('Changed theme to ADC theme') # commit msg
84 100
85 101 self._check_new_diff_menus(response, right_menu=True)
86 102
87 103 def test_commit_range_page_different_ops(self, backend):
88 104 commit_id_range = {
89 105 'hg': (
90 106 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
91 107 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
92 108 'git': (
93 109 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
94 110 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
95 111 'svn': (
96 112 '335',
97 113 '337'),
98 114 }
99 115 commit_ids = commit_id_range[backend.alias]
100 116 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
101 response = self.app.get(url(
102 controller='changeset', action='index',
103 repo_name=backend.repo_name, revision=commit_id))
117 response = self.app.get(route_path(
118 'repo_commit',
119 repo_name=backend.repo_name, commit_id=commit_id))
104 120
105 121 response.mustcontain(_shorten_commit_id(commit_ids[0]))
106 122 response.mustcontain(_shorten_commit_id(commit_ids[1]))
107 123
108 124 # svn is special
109 125 if backend.alias == 'svn':
110 126 response.mustcontain('new file 10644')
111 127 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
112 128 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
113 129 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
114 130 else:
115 131 response.mustcontain('new file 100644')
116 132 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
117 133 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
118 134
119 135 # files op files
120 136 response.mustcontain('File no longer present at commit: %s' %
121 137 _shorten_commit_id(commit_ids[1]))
122 138 response.mustcontain('Added docstrings to vcs.cli') # commit msg
123 139 response.mustcontain('Changed theme to ADC theme') # commit msg
124 140
125 141 self._check_new_diff_menus(response)
126 142
127 143 def test_combined_compare_commit_page_different_ops(self, backend):
128 144 commit_id_range = {
129 145 'hg': (
130 146 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
131 147 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
132 148 'git': (
133 149 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
134 150 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
135 151 'svn': (
136 152 '335',
137 153 '337'),
138 154 }
139 155 commit_ids = commit_id_range[backend.alias]
140 response = self.app.get(url(
141 controller='compare', action='compare',
156 response = self.app.get(route_path(
157 'repo_compare',
142 158 repo_name=backend.repo_name,
143 159 source_ref_type='rev', source_ref=commit_ids[0],
144 160 target_ref_type='rev', target_ref=commit_ids[1], ))
145 161
146 162 response.mustcontain(_shorten_commit_id(commit_ids[0]))
147 163 response.mustcontain(_shorten_commit_id(commit_ids[1]))
148 164
149 165 # files op files
150 166 response.mustcontain('File no longer present at commit: %s' %
151 167 _shorten_commit_id(commit_ids[1]))
152 168
153 169 # svn is special
154 170 if backend.alias == 'svn':
155 171 response.mustcontain('new file 10644')
156 172 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
157 173 else:
158 174 response.mustcontain('new file 100644')
159 175 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
160 176
161 177 response.mustcontain('Added docstrings to vcs.cli') # commit msg
162 178 response.mustcontain('Changed theme to ADC theme') # commit msg
163 179
164 180 self._check_new_diff_menus(response)
165 181
166 182 def test_changeset_range(self, backend):
167 183 self._check_changeset_range(
168 184 backend, self.commit_id_range, self.commit_id_range_result)
169 185
170 186 def test_changeset_range_with_initial_commit(self, backend):
171 187 commit_id_range = {
172 188 'hg': (
173 189 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
174 190 '...6cba7170863a2411822803fa77a0a264f1310b35'),
175 191 'git': (
176 192 'c1214f7e79e02fc37156ff215cd71275450cffc3'
177 193 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
178 194 'svn': '1...3',
179 195 }
180 196 commit_id_range_result = {
181 197 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
182 198 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
183 199 'svn': ['1', '2', '3'],
184 200 }
185 201 self._check_changeset_range(
186 202 backend, commit_id_range, commit_id_range_result)
187 203
188 204 def _check_changeset_range(
189 205 self, backend, commit_id_ranges, commit_id_range_result):
190 206 response = self.app.get(
191 url(controller='changeset', action='index',
207 route_path('repo_commit',
192 208 repo_name=backend.repo_name,
193 revision=commit_id_ranges[backend.alias]))
209 commit_id=commit_id_ranges[backend.alias]))
210
194 211 expected_result = commit_id_range_result[backend.alias]
195 212 response.mustcontain('{} commits'.format(len(expected_result)))
196 213 for commit_id in expected_result:
197 214 response.mustcontain(commit_id)
198 215
199 216 commit_id = {
200 217 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
201 218 'svn': '393',
202 219 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
203 220 }
204 221
205 222 commit_id_range = {
206 223 'hg': (
207 224 'a53d9201d4bc278910d416d94941b7ea007ecd52'
208 225 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
209 226 'git': (
210 227 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
211 228 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
212 229 'svn': '391...393',
213 230 }
214 231
215 232 commit_id_range_result = {
216 233 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
217 234 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
218 235 'svn': ['391', '392', '393'],
219 236 }
220 237
221 238 diffs = {
222 239 'hg': r"""diff --git a/README b/README
223 240 new file mode 120000
224 241 --- /dev/null
225 242 +++ b/README
226 243 @@ -0,0 +1,1 @@
227 244 +README.rst
228 245 \ No newline at end of file
229 246 """,
230 247 'git': r"""diff --git a/README b/README
231 248 new file mode 120000
232 249 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
233 250 --- /dev/null
234 251 +++ b/README
235 252 @@ -0,0 +1 @@
236 253 +README.rst
237 254 \ No newline at end of file
238 255 """,
239 256 'svn': """Index: README
240 257 ===================================================================
241 258 diff --git a/README b/README
242 259 new file mode 10644
243 260 --- /dev/null\t(revision 0)
244 261 +++ b/README\t(revision 393)
245 262 @@ -0,0 +1 @@
246 263 +link README.rst
247 264 \\ No newline at end of file
248 265 """,
249 266 }
250 267
251 268 patches = {
252 269 'hg': r"""# HG changeset patch
253 270 # User Marcin Kuzminski <marcin@python-works.com>
254 271 # Date 2014-01-07 12:21:40
255 272 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
256 273 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
257 274
258 275 Added a symlink
259 276
260 277 """ + diffs['hg'],
261 278 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
262 279 From: Marcin Kuzminski <marcin@python-works.com>
263 280 Date: 2014-01-07 12:22:20
264 281 Subject: [PATCH] Added a symlink
265 282
266 283 ---
267 284
268 285 """ + diffs['git'],
269 286 'svn': r"""# SVN changeset patch
270 287 # User marcin
271 288 # Date 2014-09-02 12:25:22.071142
272 289 # Revision 393
273 290
274 291 Added a symlink
275 292
276 293 """ + diffs['svn'],
277 294 }
278 295
279 296 def _check_diff_menus(self, response, right_menu=False,):
280 297 # diff menus
281 298 for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
282 299 'Raw Diff', 'Download Diff']:
283 300 response.mustcontain(elem)
284 301
285 302 # right pane diff menus
286 303 if right_menu:
287 304 for elem in ['Ignore whitespace', 'Increase context',
288 305 'Hide comments']:
289 306 response.mustcontain(elem)
290 307
291 308 def _check_new_diff_menus(self, response, right_menu=False,):
292 309 # diff menus
293 310 for elem in ['Show file before', 'Show file after',
294 311 'Raw diff', 'Download diff']:
295 312 response.mustcontain(elem)
296 313
297 314 # right pane diff menus
298 315 if right_menu:
299 316 for elem in ['Ignore whitespace', 'Increase context',
300 317 'Hide comments']:
301 318 response.mustcontain(elem)
@@ -1,203 +1,203 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-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 import pytz
22 22 import logging
23 23
24 24 from beaker.cache import cache_region
25 25 from pyramid.view import view_config
26 26 from pyramid.response import Response
27 27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
33 33 NotAnonymous, CSRFRequired)
34 34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.utils2 import str2bool, safe_int
37 37 from rhodecode.model.db import UserApiKeys, CacheKey
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class RepoFeedView(RepoAppView):
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 45
46 46 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
47 47 c.repo_info = self.db_repo
48 48
49 49 self._register_global_c(c)
50 50 self._load_defaults()
51 51 return c
52 52
53 53 def _get_config(self):
54 54 import rhodecode
55 55 config = rhodecode.CONFIG
56 56
57 57 return {
58 58 'language': 'en-us',
59 59 'feed_ttl': '5', # TTL of feed,
60 60 'feed_include_diff':
61 61 str2bool(config.get('rss_include_diff', False)),
62 62 'feed_items_per_page':
63 63 safe_int(config.get('rss_items_per_page', 20)),
64 64 'feed_diff_limit':
65 65 # we need to protect from parsing huge diffs here other way
66 66 # we can kill the server
67 67 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
68 68 }
69 69
70 70 def _load_defaults(self):
71 71 _ = self.request.translate
72 72 config = self._get_config()
73 73 # common values for feeds
74 74 self.description = _('Changes on %s repository')
75 75 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
76 76 self.language = config["language"]
77 77 self.ttl = config["feed_ttl"]
78 78 self.feed_include_diff = config['feed_include_diff']
79 79 self.feed_diff_limit = config['feed_diff_limit']
80 80 self.feed_items_per_page = config['feed_items_per_page']
81 81
82 82 def _changes(self, commit):
83 83 diff_processor = DiffProcessor(
84 84 commit.diff(), diff_limit=self.feed_diff_limit)
85 85 _parsed = diff_processor.prepare(inline_diff=False)
86 86 limited_diff = isinstance(_parsed, LimitedDiffContainer)
87 87
88 88 return _parsed, limited_diff
89 89
90 90 def _get_title(self, commit):
91 91 return h.shorter(commit.message, 160)
92 92
93 93 def _get_description(self, commit):
94 94 _renderer = self.request.get_partial_renderer(
95 95 'feed/atom_feed_entry.mako')
96 96 parsed_diff, limited_diff = self._changes(commit)
97 97 return _renderer(
98 98 'body',
99 99 commit=commit,
100 100 parsed_diff=parsed_diff,
101 101 limited_diff=limited_diff,
102 102 feed_include_diff=self.feed_include_diff,
103 103 )
104 104
105 105 def _set_timezone(self, date, tzinfo=pytz.utc):
106 106 if not getattr(date, "tzinfo", None):
107 107 date.replace(tzinfo=tzinfo)
108 108 return date
109 109
110 110 def _get_commits(self):
111 111 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
112 112
113 113 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
114 114 @HasRepoPermissionAnyDecorator(
115 115 'repository.read', 'repository.write', 'repository.admin')
116 116 @view_config(
117 117 route_name='atom_feed_home', request_method='GET',
118 118 renderer=None)
119 119 def atom(self):
120 120 """
121 121 Produce an atom-1.0 feed via feedgenerator module
122 122 """
123 123 self.load_default_context()
124 124
125 125 @cache_region('long_term')
126 126 def _generate_feed(cache_key):
127 127 feed = Atom1Feed(
128 128 title=self.title % self.db_repo_name,
129 129 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
130 130 description=self.description % self.db_repo_name,
131 131 language=self.language,
132 132 ttl=self.ttl
133 133 )
134 134
135 135 for commit in reversed(self._get_commits()):
136 136 date = self._set_timezone(commit.date)
137 137 feed.add_item(
138 138 title=self._get_title(commit),
139 139 author_name=commit.author,
140 140 description=self._get_description(commit),
141 141 link=h.route_url(
142 'changeset_home', repo_name=self.db_repo_name,
143 revision=commit.raw_id),
142 'repo_commit', repo_name=self.db_repo_name,
143 commit_id=commit.raw_id),
144 144 pubdate=date,)
145 145
146 146 return feed.mime_type, feed.writeString('utf-8')
147 147
148 148 invalidator_context = CacheKey.repo_context_cache(
149 149 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
150 150
151 151 with invalidator_context as context:
152 152 context.invalidate()
153 153 mime_type, feed = context.compute()
154 154
155 155 response = Response(feed)
156 156 response.content_type = mime_type
157 157 return response
158 158
159 159 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
160 160 @HasRepoPermissionAnyDecorator(
161 161 'repository.read', 'repository.write', 'repository.admin')
162 162 @view_config(
163 163 route_name='rss_feed_home', request_method='GET',
164 164 renderer=None)
165 165 def rss(self):
166 166 """
167 167 Produce an rss2 feed via feedgenerator module
168 168 """
169 169 self.load_default_context()
170 170
171 171 @cache_region('long_term')
172 172 def _generate_feed(cache_key):
173 173 feed = Rss201rev2Feed(
174 174 title=self.title % self.db_repo_name,
175 175 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
176 176 description=self.description % self.db_repo_name,
177 177 language=self.language,
178 178 ttl=self.ttl
179 179 )
180 180
181 181 for commit in reversed(self._get_commits()):
182 182 date = self._set_timezone(commit.date)
183 183 feed.add_item(
184 184 title=self._get_title(commit),
185 185 author_name=commit.author,
186 186 description=self._get_description(commit),
187 187 link=h.route_url(
188 'changeset_home', repo_name=self.db_repo_name,
189 revision=commit.raw_id),
188 'repo_commit', repo_name=self.db_repo_name,
189 commit_id=commit.raw_id),
190 190 pubdate=date,)
191 191
192 192 return feed.mime_type, feed.writeString('utf-8')
193 193
194 194 invalidator_context = CacheKey.repo_context_cache(
195 195 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
196 196
197 197 with invalidator_context as context:
198 198 context.invalidate()
199 199 mime_type, feed = context.compute()
200 200
201 201 response = Response(feed)
202 202 response.content_type = mime_type
203 203 return response
@@ -1,1278 +1,1278 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import RepoAppView
34 34
35 35 from rhodecode.controllers.utils import parse_path_ref
36 36 from rhodecode.lib import diffs, helpers as h, caches
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.exceptions import NonRelativePathError
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils2 import (
42 42 convert_line_endings, detect_mode, safe_str, str2bool)
43 43 from rhodecode.lib.auth import (
44 44 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 45 from rhodecode.lib.vcs import path as vcspath
46 46 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 47 from rhodecode.lib.vcs.conf import settings
48 48 from rhodecode.lib.vcs.nodes import FileNode
49 49 from rhodecode.lib.vcs.exceptions import (
50 50 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 52 NodeDoesNotExistError, CommitError, NodeError)
53 53
54 54 from rhodecode.model.scm import ScmModel
55 55 from rhodecode.model.db import Repository
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoFilesView(RepoAppView):
61 61
62 62 @staticmethod
63 63 def adjust_file_path_for_svn(f_path, repo):
64 64 """
65 65 Computes the relative path of `f_path`.
66 66
67 67 This is mainly based on prefix matching of the recognized tags and
68 68 branches in the underlying repository.
69 69 """
70 70 tags_and_branches = itertools.chain(
71 71 repo.branches.iterkeys(),
72 72 repo.tags.iterkeys())
73 73 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74 74
75 75 for name in tags_and_branches:
76 76 if f_path.startswith('{}/'.format(name)):
77 77 f_path = vcspath.relpath(f_path, name)
78 78 break
79 79 return f_path
80 80
81 81 def load_default_context(self):
82 82 c = self._get_local_tmpl_context(include_app_defaults=True)
83 83
84 84 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
85 85 c.repo_info = self.db_repo
86 86 c.rhodecode_repo = self.rhodecode_vcs_repo
87 87
88 88 self._register_global_c(c)
89 89 return c
90 90
91 91 def _ensure_not_locked(self):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id='tip')
103 103 raise HTTPFound(files_url)
104 104
105 105 def _get_commit_and_path(self):
106 106 default_commit_id = self.db_repo.landing_rev[1]
107 107 default_f_path = '/'
108 108
109 109 commit_id = self.request.matchdict.get(
110 110 'commit_id', default_commit_id)
111 111 f_path = self._get_f_path(self.request.matchdict, default_f_path)
112 112 return commit_id, f_path
113 113
114 114 def _get_default_encoding(self, c):
115 115 enc_list = getattr(c, 'default_encodings', [])
116 116 return enc_list[0] if enc_list else 'UTF-8'
117 117
118 118 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
119 119 """
120 120 This is a safe way to get commit. If an error occurs it redirects to
121 121 tip with proper message
122 122
123 123 :param commit_id: id of commit to fetch
124 124 :param redirect_after: toggle redirection
125 125 """
126 126 _ = self.request.translate
127 127
128 128 try:
129 129 return self.rhodecode_vcs_repo.get_commit(commit_id)
130 130 except EmptyRepositoryError:
131 131 if not redirect_after:
132 132 return None
133 133
134 134 _url = h.route_path(
135 135 'repo_files_add_file',
136 136 repo_name=self.db_repo_name, commit_id=0, f_path='',
137 137 _anchor='edit')
138 138
139 139 if h.HasRepoPermissionAny(
140 140 'repository.write', 'repository.admin')(self.db_repo_name):
141 141 add_new = h.link_to(
142 142 _('Click here to add a new file.'), _url, class_="alert-link")
143 143 else:
144 144 add_new = ""
145 145
146 146 h.flash(h.literal(
147 147 _('There are no files yet. %s') % add_new), category='warning')
148 148 raise HTTPFound(
149 149 h.route_path('repo_summary', repo_name=self.db_repo_name))
150 150
151 151 except (CommitDoesNotExistError, LookupError):
152 152 msg = _('No such commit exists for this repository')
153 153 h.flash(msg, category='error')
154 154 raise HTTPNotFound()
155 155 except RepositoryError as e:
156 156 h.flash(safe_str(h.escape(e)), category='error')
157 157 raise HTTPNotFound()
158 158
159 159 def _get_filenode_or_redirect(self, commit_obj, path):
160 160 """
161 161 Returns file_node, if error occurs or given path is directory,
162 162 it'll redirect to top level path
163 163 """
164 164 _ = self.request.translate
165 165
166 166 try:
167 167 file_node = commit_obj.get_node(path)
168 168 if file_node.is_dir():
169 169 raise RepositoryError('The given path is a directory')
170 170 except CommitDoesNotExistError:
171 171 log.exception('No such commit exists for this repository')
172 172 h.flash(_('No such commit exists for this repository'), category='error')
173 173 raise HTTPNotFound()
174 174 except RepositoryError as e:
175 175 log.warning('Repository error while fetching '
176 176 'filenode `%s`. Err:%s', path, e)
177 177 h.flash(safe_str(h.escape(e)), category='error')
178 178 raise HTTPNotFound()
179 179
180 180 return file_node
181 181
182 182 def _is_valid_head(self, commit_id, repo):
183 183 # check if commit is a branch identifier- basically we cannot
184 184 # create multiple heads via file editing
185 185 valid_heads = repo.branches.keys() + repo.branches.values()
186 186
187 187 if h.is_svn(repo) and not repo.is_empty():
188 188 # Note: Subversion only has one head, we add it here in case there
189 189 # is no branch matched.
190 190 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
191 191
192 192 # check if commit is a branch name or branch hash
193 193 return commit_id in valid_heads
194 194
195 195 def _get_tree_cache_manager(self, namespace_type):
196 196 _namespace = caches.get_repo_namespace_key(
197 197 namespace_type, self.db_repo_name)
198 198 return caches.get_cache_manager('repo_cache_long', _namespace)
199 199
200 200 def _get_tree_at_commit(
201 201 self, c, commit_id, f_path, full_load=False, force=False):
202 202 def _cached_tree():
203 203 log.debug('Generating cached file tree for %s, %s, %s',
204 204 self.db_repo_name, commit_id, f_path)
205 205
206 206 c.full_load = full_load
207 207 return render(
208 208 'rhodecode:templates/files/files_browser_tree.mako',
209 209 self._get_template_context(c), self.request)
210 210
211 211 cache_manager = self._get_tree_cache_manager(caches.FILE_TREE)
212 212
213 213 cache_key = caches.compute_key_from_params(
214 214 self.db_repo_name, commit_id, f_path)
215 215
216 216 if force:
217 217 # we want to force recompute of caches
218 218 cache_manager.remove_value(cache_key)
219 219
220 220 return cache_manager.get(cache_key, createfunc=_cached_tree)
221 221
222 222 def _get_archive_spec(self, fname):
223 223 log.debug('Detecting archive spec for: `%s`', fname)
224 224
225 225 fileformat = None
226 226 ext = None
227 227 content_type = None
228 228 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
229 229 content_type, extension = ext_data
230 230
231 231 if fname.endswith(extension):
232 232 fileformat = a_type
233 233 log.debug('archive is of type: %s', fileformat)
234 234 ext = extension
235 235 break
236 236
237 237 if not fileformat:
238 238 raise ValueError()
239 239
240 240 # left over part of whole fname is the commit
241 241 commit_id = fname[:-len(ext)]
242 242
243 243 return commit_id, ext, fileformat, content_type
244 244
245 245 @LoginRequired()
246 246 @HasRepoPermissionAnyDecorator(
247 247 'repository.read', 'repository.write', 'repository.admin')
248 248 @view_config(
249 249 route_name='repo_archivefile', request_method='GET',
250 250 renderer=None)
251 251 def repo_archivefile(self):
252 252 # archive cache config
253 253 from rhodecode import CONFIG
254 254 _ = self.request.translate
255 255 self.load_default_context()
256 256
257 257 fname = self.request.matchdict['fname']
258 258 subrepos = self.request.GET.get('subrepos') == 'true'
259 259
260 260 if not self.db_repo.enable_downloads:
261 261 return Response(_('Downloads disabled'))
262 262
263 263 try:
264 264 commit_id, ext, fileformat, content_type = \
265 265 self._get_archive_spec(fname)
266 266 except ValueError:
267 267 return Response(_('Unknown archive type for: `{}`').format(fname))
268 268
269 269 try:
270 270 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
271 271 except CommitDoesNotExistError:
272 272 return Response(_('Unknown commit_id %s') % commit_id)
273 273 except EmptyRepositoryError:
274 274 return Response(_('Empty repository'))
275 275
276 276 archive_name = '%s-%s%s%s' % (
277 277 safe_str(self.db_repo_name.replace('/', '_')),
278 278 '-sub' if subrepos else '',
279 279 safe_str(commit.short_id), ext)
280 280
281 281 use_cached_archive = False
282 282 archive_cache_enabled = CONFIG.get(
283 283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284 284
285 285 if archive_cache_enabled:
286 286 # check if we it's ok to write
287 287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 288 os.makedirs(CONFIG['archive_cache_dir'])
289 289 cached_archive_path = os.path.join(
290 290 CONFIG['archive_cache_dir'], archive_name)
291 291 if os.path.isfile(cached_archive_path):
292 292 log.debug('Found cached archive in %s', cached_archive_path)
293 293 fd, archive = None, cached_archive_path
294 294 use_cached_archive = True
295 295 else:
296 296 log.debug('Archive %s is not yet cached', archive_name)
297 297
298 298 if not use_cached_archive:
299 299 # generate new archive
300 300 fd, archive = tempfile.mkstemp()
301 301 log.debug('Creating new temp archive in %s', archive)
302 302 try:
303 303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 304 except ImproperArchiveTypeError:
305 305 return _('Unknown archive type')
306 306 if archive_cache_enabled:
307 307 # if we generated the archive and we have cache enabled
308 308 # let's use this for future
309 309 log.debug('Storing new archive in %s', cached_archive_path)
310 310 shutil.move(archive, cached_archive_path)
311 311 archive = cached_archive_path
312 312
313 313 # store download action
314 314 audit_logger.store_web(
315 315 'repo.archive.download', action_data={
316 316 'user_agent': self.request.user_agent,
317 317 'archive_name': archive_name,
318 318 'archive_spec': fname,
319 319 'archive_cached': use_cached_archive},
320 320 user=self._rhodecode_user,
321 321 repo=self.db_repo,
322 322 commit=True
323 323 )
324 324
325 325 def get_chunked_archive(archive):
326 326 with open(archive, 'rb') as stream:
327 327 while True:
328 328 data = stream.read(16 * 1024)
329 329 if not data:
330 330 if fd: # fd means we used temporary file
331 331 os.close(fd)
332 332 if not archive_cache_enabled:
333 333 log.debug('Destroying temp archive %s', archive)
334 334 os.remove(archive)
335 335 break
336 336 yield data
337 337
338 338 response = Response(app_iter=get_chunked_archive(archive))
339 339 response.content_disposition = str(
340 340 'attachment; filename=%s' % archive_name)
341 341 response.content_type = str(content_type)
342 342
343 343 return response
344 344
345 345 def _get_file_node(self, commit_id, f_path):
346 346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 348 try:
349 349 node = commit.get_node(f_path)
350 350 if node.is_dir():
351 351 raise NodeError('%s path is a %s not a file'
352 352 % (node, type(node)))
353 353 except NodeDoesNotExistError:
354 354 commit = EmptyCommit(
355 355 commit_id=commit_id,
356 356 idx=commit.idx,
357 357 repo=commit.repository,
358 358 alias=commit.repository.alias,
359 359 message=commit.message,
360 360 author=commit.author,
361 361 date=commit.date)
362 362 node = FileNode(f_path, '', commit=commit)
363 363 else:
364 364 commit = EmptyCommit(
365 365 repo=self.rhodecode_vcs_repo,
366 366 alias=self.rhodecode_vcs_repo.alias)
367 367 node = FileNode(f_path, '', commit=commit)
368 368 return node
369 369
370 370 @LoginRequired()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @view_config(
374 374 route_name='repo_files_diff', request_method='GET',
375 375 renderer=None)
376 376 def repo_files_diff(self):
377 377 c = self.load_default_context()
378 378 f_path = self._get_f_path(self.request.matchdict)
379 379 diff1 = self.request.GET.get('diff1', '')
380 380 diff2 = self.request.GET.get('diff2', '')
381 381
382 382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383 383
384 384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 385 line_context = self.request.GET.get('context', 3)
386 386
387 387 if not any((diff1, diff2)):
388 388 h.flash(
389 389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 390 category='error')
391 391 raise HTTPBadRequest()
392 392
393 393 c.action = self.request.GET.get('diff')
394 394 if c.action not in ['download', 'raw']:
395 395 compare_url = h.url(
396 396 'compare_url', repo_name=self.db_repo_name,
397 397 source_ref_type='rev',
398 398 source_ref=diff1,
399 399 target_repo=self.db_repo_name,
400 400 target_ref_type='rev',
401 401 target_ref=diff2,
402 402 f_path=f_path)
403 403 # redirect to new view if we render diff
404 404 raise HTTPFound(compare_url)
405 405
406 406 try:
407 407 node1 = self._get_file_node(diff1, path1)
408 408 node2 = self._get_file_node(diff2, f_path)
409 409 except (RepositoryError, NodeError):
410 410 log.exception("Exception while trying to get node from repository")
411 411 raise HTTPFound(
412 412 h.route_path('repo_files', repo_name=self.db_repo_name,
413 413 commit_id='tip', f_path=f_path))
414 414
415 415 if all(isinstance(node.commit, EmptyCommit)
416 416 for node in (node1, node2)):
417 417 raise HTTPNotFound()
418 418
419 419 c.commit_1 = node1.commit
420 420 c.commit_2 = node2.commit
421 421
422 422 if c.action == 'download':
423 423 _diff = diffs.get_gitdiff(node1, node2,
424 424 ignore_whitespace=ignore_whitespace,
425 425 context=line_context)
426 426 diff = diffs.DiffProcessor(_diff, format='gitdiff')
427 427
428 428 response = Response(diff.as_raw())
429 429 response.content_type = 'text/plain'
430 430 response.content_disposition = (
431 431 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
432 432 )
433 433 charset = self._get_default_encoding(c)
434 434 if charset:
435 435 response.charset = charset
436 436 return response
437 437
438 438 elif c.action == 'raw':
439 439 _diff = diffs.get_gitdiff(node1, node2,
440 440 ignore_whitespace=ignore_whitespace,
441 441 context=line_context)
442 442 diff = diffs.DiffProcessor(_diff, format='gitdiff')
443 443
444 444 response = Response(diff.as_raw())
445 445 response.content_type = 'text/plain'
446 446 charset = self._get_default_encoding(c)
447 447 if charset:
448 448 response.charset = charset
449 449 return response
450 450
451 451 # in case we ever end up here
452 452 raise HTTPNotFound()
453 453
454 454 @LoginRequired()
455 455 @HasRepoPermissionAnyDecorator(
456 456 'repository.read', 'repository.write', 'repository.admin')
457 457 @view_config(
458 458 route_name='repo_files_diff_2way_redirect', request_method='GET',
459 459 renderer=None)
460 460 def repo_files_diff_2way_redirect(self):
461 461 """
462 462 Kept only to make OLD links work
463 463 """
464 464 f_path = self._get_f_path(self.request.matchdict)
465 465 diff1 = self.request.GET.get('diff1', '')
466 466 diff2 = self.request.GET.get('diff2', '')
467 467
468 468 if not any((diff1, diff2)):
469 469 h.flash(
470 470 'Need query parameter "diff1" or "diff2" to generate a diff.',
471 471 category='error')
472 472 raise HTTPBadRequest()
473 473
474 474 compare_url = h.url(
475 475 'compare_url', repo_name=self.db_repo_name,
476 476 source_ref_type='rev',
477 477 source_ref=diff1,
478 478 target_repo=self.db_repo_name,
479 479 target_ref_type='rev',
480 480 target_ref=diff2,
481 481 f_path=f_path,
482 482 diffmode='sideside')
483 483 raise HTTPFound(compare_url)
484 484
485 485 @LoginRequired()
486 486 @HasRepoPermissionAnyDecorator(
487 487 'repository.read', 'repository.write', 'repository.admin')
488 488 @view_config(
489 489 route_name='repo_files', request_method='GET',
490 490 renderer=None)
491 491 @view_config(
492 492 route_name='repo_files:default_path', request_method='GET',
493 493 renderer=None)
494 494 @view_config(
495 495 route_name='repo_files:default_commit', request_method='GET',
496 496 renderer=None)
497 497 @view_config(
498 498 route_name='repo_files:rendered', request_method='GET',
499 499 renderer=None)
500 500 @view_config(
501 501 route_name='repo_files:annotated', request_method='GET',
502 502 renderer=None)
503 503 def repo_files(self):
504 504 c = self.load_default_context()
505 505
506 506 view_name = getattr(self.request.matched_route, 'name', None)
507 507
508 508 c.annotate = view_name == 'repo_files:annotated'
509 509 # default is false, but .rst/.md files later are auto rendered, we can
510 510 # overwrite auto rendering by setting this GET flag
511 511 c.renderer = view_name == 'repo_files:rendered' or \
512 512 not self.request.GET.get('no-render', False)
513 513
514 514 # redirect to given commit_id from form if given
515 515 get_commit_id = self.request.GET.get('at_rev', None)
516 516 if get_commit_id:
517 517 self._get_commit_or_redirect(get_commit_id)
518 518
519 519 commit_id, f_path = self._get_commit_and_path()
520 520 c.commit = self._get_commit_or_redirect(commit_id)
521 521 c.branch = self.request.GET.get('branch', None)
522 522 c.f_path = f_path
523 523
524 524 # prev link
525 525 try:
526 526 prev_commit = c.commit.prev(c.branch)
527 527 c.prev_commit = prev_commit
528 528 c.url_prev = h.route_path(
529 529 'repo_files', repo_name=self.db_repo_name,
530 530 commit_id=prev_commit.raw_id, f_path=f_path)
531 531 if c.branch:
532 532 c.url_prev += '?branch=%s' % c.branch
533 533 except (CommitDoesNotExistError, VCSError):
534 534 c.url_prev = '#'
535 535 c.prev_commit = EmptyCommit()
536 536
537 537 # next link
538 538 try:
539 539 next_commit = c.commit.next(c.branch)
540 540 c.next_commit = next_commit
541 541 c.url_next = h.route_path(
542 542 'repo_files', repo_name=self.db_repo_name,
543 543 commit_id=next_commit.raw_id, f_path=f_path)
544 544 if c.branch:
545 545 c.url_next += '?branch=%s' % c.branch
546 546 except (CommitDoesNotExistError, VCSError):
547 547 c.url_next = '#'
548 548 c.next_commit = EmptyCommit()
549 549
550 550 # files or dirs
551 551 try:
552 552 c.file = c.commit.get_node(f_path)
553 553 c.file_author = True
554 554 c.file_tree = ''
555 555
556 556 # load file content
557 557 if c.file.is_file():
558 558 c.lf_node = c.file.get_largefile_node()
559 559
560 560 c.file_source_page = 'true'
561 561 c.file_last_commit = c.file.last_commit
562 562 if c.file.size < c.visual.cut_off_limit_diff:
563 563 if c.annotate: # annotation has precedence over renderer
564 564 c.annotated_lines = filenode_as_annotated_lines_tokens(
565 565 c.file
566 566 )
567 567 else:
568 568 c.renderer = (
569 569 c.renderer and h.renderer_from_filename(c.file.path)
570 570 )
571 571 if not c.renderer:
572 572 c.lines = filenode_as_lines_tokens(c.file)
573 573
574 574 c.on_branch_head = self._is_valid_head(
575 575 commit_id, self.rhodecode_vcs_repo)
576 576
577 577 branch = c.commit.branch if (
578 578 c.commit.branch and '/' not in c.commit.branch) else None
579 579 c.branch_or_raw_id = branch or c.commit.raw_id
580 580 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
581 581
582 582 author = c.file_last_commit.author
583 583 c.authors = [[
584 584 h.email(author),
585 585 h.person(author, 'username_or_name_or_email'),
586 586 1
587 587 ]]
588 588
589 589 else: # load tree content at path
590 590 c.file_source_page = 'false'
591 591 c.authors = []
592 592 # this loads a simple tree without metadata to speed things up
593 593 # later via ajax we call repo_nodetree_full and fetch whole
594 594 c.file_tree = self._get_tree_at_commit(
595 595 c, c.commit.raw_id, f_path)
596 596
597 597 except RepositoryError as e:
598 598 h.flash(safe_str(h.escape(e)), category='error')
599 599 raise HTTPNotFound()
600 600
601 601 if self.request.environ.get('HTTP_X_PJAX'):
602 602 html = render('rhodecode:templates/files/files_pjax.mako',
603 603 self._get_template_context(c), self.request)
604 604 else:
605 605 html = render('rhodecode:templates/files/files.mako',
606 606 self._get_template_context(c), self.request)
607 607 return Response(html)
608 608
609 609 @HasRepoPermissionAnyDecorator(
610 610 'repository.read', 'repository.write', 'repository.admin')
611 611 @view_config(
612 612 route_name='repo_files:annotated_previous', request_method='GET',
613 613 renderer=None)
614 614 def repo_files_annotated_previous(self):
615 615 self.load_default_context()
616 616
617 617 commit_id, f_path = self._get_commit_and_path()
618 618 commit = self._get_commit_or_redirect(commit_id)
619 619 prev_commit_id = commit.raw_id
620 620 line_anchor = self.request.GET.get('line_anchor')
621 621 is_file = False
622 622 try:
623 623 _file = commit.get_node(f_path)
624 624 is_file = _file.is_file()
625 625 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
626 626 pass
627 627
628 628 if is_file:
629 629 history = commit.get_file_history(f_path)
630 630 prev_commit_id = history[1].raw_id \
631 631 if len(history) > 1 else prev_commit_id
632 632 prev_url = h.route_path(
633 633 'repo_files:annotated', repo_name=self.db_repo_name,
634 634 commit_id=prev_commit_id, f_path=f_path,
635 635 _anchor='L{}'.format(line_anchor))
636 636
637 637 raise HTTPFound(prev_url)
638 638
639 639 @LoginRequired()
640 640 @HasRepoPermissionAnyDecorator(
641 641 'repository.read', 'repository.write', 'repository.admin')
642 642 @view_config(
643 643 route_name='repo_nodetree_full', request_method='GET',
644 644 renderer=None, xhr=True)
645 645 @view_config(
646 646 route_name='repo_nodetree_full:default_path', request_method='GET',
647 647 renderer=None, xhr=True)
648 648 def repo_nodetree_full(self):
649 649 """
650 650 Returns rendered html of file tree that contains commit date,
651 651 author, commit_id for the specified combination of
652 652 repo, commit_id and file path
653 653 """
654 654 c = self.load_default_context()
655 655
656 656 commit_id, f_path = self._get_commit_and_path()
657 657 commit = self._get_commit_or_redirect(commit_id)
658 658 try:
659 659 dir_node = commit.get_node(f_path)
660 660 except RepositoryError as e:
661 661 return Response('error: {}'.format(safe_str(e)))
662 662
663 663 if dir_node.is_file():
664 664 return Response('')
665 665
666 666 c.file = dir_node
667 667 c.commit = commit
668 668
669 669 # using force=True here, make a little trick. We flush the cache and
670 670 # compute it using the same key as without previous full_load, so now
671 671 # the fully loaded tree is now returned instead of partial,
672 672 # and we store this in caches
673 673 html = self._get_tree_at_commit(
674 674 c, commit.raw_id, dir_node.path, full_load=True, force=True)
675 675
676 676 return Response(html)
677 677
678 678 def _get_attachement_disposition(self, f_path):
679 679 return 'attachment; filename=%s' % \
680 680 safe_str(f_path.split(Repository.NAME_SEP)[-1])
681 681
682 682 @LoginRequired()
683 683 @HasRepoPermissionAnyDecorator(
684 684 'repository.read', 'repository.write', 'repository.admin')
685 685 @view_config(
686 686 route_name='repo_file_raw', request_method='GET',
687 687 renderer=None)
688 688 def repo_file_raw(self):
689 689 """
690 690 Action for show as raw, some mimetypes are "rendered",
691 691 those include images, icons.
692 692 """
693 693 c = self.load_default_context()
694 694
695 695 commit_id, f_path = self._get_commit_and_path()
696 696 commit = self._get_commit_or_redirect(commit_id)
697 697 file_node = self._get_filenode_or_redirect(commit, f_path)
698 698
699 699 raw_mimetype_mapping = {
700 700 # map original mimetype to a mimetype used for "show as raw"
701 701 # you can also provide a content-disposition to override the
702 702 # default "attachment" disposition.
703 703 # orig_type: (new_type, new_dispo)
704 704
705 705 # show images inline:
706 706 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
707 707 # for example render an SVG with javascript inside or even render
708 708 # HTML.
709 709 'image/x-icon': ('image/x-icon', 'inline'),
710 710 'image/png': ('image/png', 'inline'),
711 711 'image/gif': ('image/gif', 'inline'),
712 712 'image/jpeg': ('image/jpeg', 'inline'),
713 713 'application/pdf': ('application/pdf', 'inline'),
714 714 }
715 715
716 716 mimetype = file_node.mimetype
717 717 try:
718 718 mimetype, disposition = raw_mimetype_mapping[mimetype]
719 719 except KeyError:
720 720 # we don't know anything special about this, handle it safely
721 721 if file_node.is_binary:
722 722 # do same as download raw for binary files
723 723 mimetype, disposition = 'application/octet-stream', 'attachment'
724 724 else:
725 725 # do not just use the original mimetype, but force text/plain,
726 726 # otherwise it would serve text/html and that might be unsafe.
727 727 # Note: underlying vcs library fakes text/plain mimetype if the
728 728 # mimetype can not be determined and it thinks it is not
729 729 # binary.This might lead to erroneous text display in some
730 730 # cases, but helps in other cases, like with text files
731 731 # without extension.
732 732 mimetype, disposition = 'text/plain', 'inline'
733 733
734 734 if disposition == 'attachment':
735 735 disposition = self._get_attachement_disposition(f_path)
736 736
737 737 def stream_node():
738 738 yield file_node.raw_bytes
739 739
740 740 response = Response(app_iter=stream_node())
741 741 response.content_disposition = disposition
742 742 response.content_type = mimetype
743 743
744 744 charset = self._get_default_encoding(c)
745 745 if charset:
746 746 response.charset = charset
747 747
748 748 return response
749 749
750 750 @LoginRequired()
751 751 @HasRepoPermissionAnyDecorator(
752 752 'repository.read', 'repository.write', 'repository.admin')
753 753 @view_config(
754 754 route_name='repo_file_download', request_method='GET',
755 755 renderer=None)
756 756 @view_config(
757 757 route_name='repo_file_download:legacy', request_method='GET',
758 758 renderer=None)
759 759 def repo_file_download(self):
760 760 c = self.load_default_context()
761 761
762 762 commit_id, f_path = self._get_commit_and_path()
763 763 commit = self._get_commit_or_redirect(commit_id)
764 764 file_node = self._get_filenode_or_redirect(commit, f_path)
765 765
766 766 if self.request.GET.get('lf'):
767 767 # only if lf get flag is passed, we download this file
768 768 # as LFS/Largefile
769 769 lf_node = file_node.get_largefile_node()
770 770 if lf_node:
771 771 # overwrite our pointer with the REAL large-file
772 772 file_node = lf_node
773 773
774 774 disposition = self._get_attachement_disposition(f_path)
775 775
776 776 def stream_node():
777 777 yield file_node.raw_bytes
778 778
779 779 response = Response(app_iter=stream_node())
780 780 response.content_disposition = disposition
781 781 response.content_type = file_node.mimetype
782 782
783 783 charset = self._get_default_encoding(c)
784 784 if charset:
785 785 response.charset = charset
786 786
787 787 return response
788 788
789 789 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
790 790 def _cached_nodes():
791 791 log.debug('Generating cached nodelist for %s, %s, %s',
792 792 repo_name, commit_id, f_path)
793 793 _d, _f = ScmModel().get_nodes(
794 794 repo_name, commit_id, f_path, flat=False)
795 795 return _d + _f
796 796
797 797 cache_manager = self._get_tree_cache_manager(caches.FILE_SEARCH_TREE_META)
798 798
799 799 cache_key = caches.compute_key_from_params(
800 800 repo_name, commit_id, f_path)
801 801 return cache_manager.get(cache_key, createfunc=_cached_nodes)
802 802
803 803 @LoginRequired()
804 804 @HasRepoPermissionAnyDecorator(
805 805 'repository.read', 'repository.write', 'repository.admin')
806 806 @view_config(
807 807 route_name='repo_files_nodelist', request_method='GET',
808 808 renderer='json_ext', xhr=True)
809 809 def repo_nodelist(self):
810 810 self.load_default_context()
811 811
812 812 commit_id, f_path = self._get_commit_and_path()
813 813 commit = self._get_commit_or_redirect(commit_id)
814 814
815 815 metadata = self._get_nodelist_at_commit(
816 816 self.db_repo_name, commit.raw_id, f_path)
817 817 return {'nodes': metadata}
818 818
819 819 def _create_references(
820 820 self, branches_or_tags, symbolic_reference, f_path):
821 821 items = []
822 822 for name, commit_id in branches_or_tags.items():
823 823 sym_ref = symbolic_reference(commit_id, name, f_path)
824 824 items.append((sym_ref, name))
825 825 return items
826 826
827 827 def _symbolic_reference(self, commit_id, name, f_path):
828 828 return commit_id
829 829
830 830 def _symbolic_reference_svn(self, commit_id, name, f_path):
831 831 new_f_path = vcspath.join(name, f_path)
832 832 return u'%s@%s' % (new_f_path, commit_id)
833 833
834 834 def _get_node_history(self, commit_obj, f_path, commits=None):
835 835 """
836 836 get commit history for given node
837 837
838 838 :param commit_obj: commit to calculate history
839 839 :param f_path: path for node to calculate history for
840 840 :param commits: if passed don't calculate history and take
841 841 commits defined in this list
842 842 """
843 843 _ = self.request.translate
844 844
845 845 # calculate history based on tip
846 846 tip = self.rhodecode_vcs_repo.get_commit()
847 847 if commits is None:
848 848 pre_load = ["author", "branch"]
849 849 try:
850 850 commits = tip.get_file_history(f_path, pre_load=pre_load)
851 851 except (NodeDoesNotExistError, CommitError):
852 852 # this node is not present at tip!
853 853 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
854 854
855 855 history = []
856 856 commits_group = ([], _("Changesets"))
857 857 for commit in commits:
858 858 branch = ' (%s)' % commit.branch if commit.branch else ''
859 859 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
860 860 commits_group[0].append((commit.raw_id, n_desc,))
861 861 history.append(commits_group)
862 862
863 863 symbolic_reference = self._symbolic_reference
864 864
865 865 if self.rhodecode_vcs_repo.alias == 'svn':
866 866 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
867 867 f_path, self.rhodecode_vcs_repo)
868 868 if adjusted_f_path != f_path:
869 869 log.debug(
870 870 'Recognized svn tag or branch in file "%s", using svn '
871 871 'specific symbolic references', f_path)
872 872 f_path = adjusted_f_path
873 873 symbolic_reference = self._symbolic_reference_svn
874 874
875 875 branches = self._create_references(
876 876 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
877 877 branches_group = (branches, _("Branches"))
878 878
879 879 tags = self._create_references(
880 880 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
881 881 tags_group = (tags, _("Tags"))
882 882
883 883 history.append(branches_group)
884 884 history.append(tags_group)
885 885
886 886 return history, commits
887 887
888 888 @LoginRequired()
889 889 @HasRepoPermissionAnyDecorator(
890 890 'repository.read', 'repository.write', 'repository.admin')
891 891 @view_config(
892 892 route_name='repo_file_history', request_method='GET',
893 893 renderer='json_ext')
894 894 def repo_file_history(self):
895 895 self.load_default_context()
896 896
897 897 commit_id, f_path = self._get_commit_and_path()
898 898 commit = self._get_commit_or_redirect(commit_id)
899 899 file_node = self._get_filenode_or_redirect(commit, f_path)
900 900
901 901 if file_node.is_file():
902 902 file_history, _hist = self._get_node_history(commit, f_path)
903 903
904 904 res = []
905 905 for obj in file_history:
906 906 res.append({
907 907 'text': obj[1],
908 908 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
909 909 })
910 910
911 911 data = {
912 912 'more': False,
913 913 'results': res
914 914 }
915 915 return data
916 916
917 917 log.warning('Cannot fetch history for directory')
918 918 raise HTTPBadRequest()
919 919
920 920 @LoginRequired()
921 921 @HasRepoPermissionAnyDecorator(
922 922 'repository.read', 'repository.write', 'repository.admin')
923 923 @view_config(
924 924 route_name='repo_file_authors', request_method='GET',
925 925 renderer='rhodecode:templates/files/file_authors_box.mako')
926 926 def repo_file_authors(self):
927 927 c = self.load_default_context()
928 928
929 929 commit_id, f_path = self._get_commit_and_path()
930 930 commit = self._get_commit_or_redirect(commit_id)
931 931 file_node = self._get_filenode_or_redirect(commit, f_path)
932 932
933 933 if not file_node.is_file():
934 934 raise HTTPBadRequest()
935 935
936 936 c.file_last_commit = file_node.last_commit
937 937 if self.request.GET.get('annotate') == '1':
938 938 # use _hist from annotation if annotation mode is on
939 939 commit_ids = set(x[1] for x in file_node.annotate)
940 940 _hist = (
941 941 self.rhodecode_vcs_repo.get_commit(commit_id)
942 942 for commit_id in commit_ids)
943 943 else:
944 944 _f_history, _hist = self._get_node_history(commit, f_path)
945 945 c.file_author = False
946 946
947 947 unique = collections.OrderedDict()
948 948 for commit in _hist:
949 949 author = commit.author
950 950 if author not in unique:
951 951 unique[commit.author] = [
952 952 h.email(author),
953 953 h.person(author, 'username_or_name_or_email'),
954 954 1 # counter
955 955 ]
956 956
957 957 else:
958 958 # increase counter
959 959 unique[commit.author][2] += 1
960 960
961 961 c.authors = [val for val in unique.values()]
962 962
963 963 return self._get_template_context(c)
964 964
965 965 @LoginRequired()
966 966 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
967 967 @view_config(
968 968 route_name='repo_files_remove_file', request_method='GET',
969 969 renderer='rhodecode:templates/files/files_delete.mako')
970 970 def repo_files_remove_file(self):
971 971 _ = self.request.translate
972 972 c = self.load_default_context()
973 973 commit_id, f_path = self._get_commit_and_path()
974 974
975 975 self._ensure_not_locked()
976 976
977 977 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
978 978 h.flash(_('You can only delete files with commit '
979 979 'being a valid branch '), category='warning')
980 980 raise HTTPFound(
981 981 h.route_path('repo_files',
982 982 repo_name=self.db_repo_name, commit_id='tip',
983 983 f_path=f_path))
984 984
985 985 c.commit = self._get_commit_or_redirect(commit_id)
986 986 c.file = self._get_filenode_or_redirect(c.commit, f_path)
987 987
988 988 c.default_message = _(
989 989 'Deleted file {} via RhodeCode Enterprise').format(f_path)
990 990 c.f_path = f_path
991 991
992 992 return self._get_template_context(c)
993 993
994 994 @LoginRequired()
995 995 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
996 996 @CSRFRequired()
997 997 @view_config(
998 998 route_name='repo_files_delete_file', request_method='POST',
999 999 renderer=None)
1000 1000 def repo_files_delete_file(self):
1001 1001 _ = self.request.translate
1002 1002
1003 1003 c = self.load_default_context()
1004 1004 commit_id, f_path = self._get_commit_and_path()
1005 1005
1006 1006 self._ensure_not_locked()
1007 1007
1008 1008 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1009 1009 h.flash(_('You can only delete files with commit '
1010 1010 'being a valid branch '), category='warning')
1011 1011 raise HTTPFound(
1012 1012 h.route_path('repo_files',
1013 1013 repo_name=self.db_repo_name, commit_id='tip',
1014 1014 f_path=f_path))
1015 1015
1016 1016 c.commit = self._get_commit_or_redirect(commit_id)
1017 1017 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1018 1018
1019 1019 c.default_message = _(
1020 1020 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1021 1021 c.f_path = f_path
1022 1022 node_path = f_path
1023 1023 author = self._rhodecode_db_user.full_contact
1024 1024 message = self.request.POST.get('message') or c.default_message
1025 1025 try:
1026 1026 nodes = {
1027 1027 node_path: {
1028 1028 'content': ''
1029 1029 }
1030 1030 }
1031 1031 ScmModel().delete_nodes(
1032 1032 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1033 1033 message=message,
1034 1034 nodes=nodes,
1035 1035 parent_commit=c.commit,
1036 1036 author=author,
1037 1037 )
1038 1038
1039 1039 h.flash(
1040 1040 _('Successfully deleted file `{}`').format(
1041 1041 h.escape(f_path)), category='success')
1042 1042 except Exception:
1043 1043 log.exception('Error during commit operation')
1044 1044 h.flash(_('Error occurred during commit'), category='error')
1045 1045 raise HTTPFound(
1046 h.route_path('changeset_home', repo_name=self.db_repo_name,
1047 revision='tip'))
1046 h.route_path('repo_commit', repo_name=self.db_repo_name,
1047 commit_id='tip'))
1048 1048
1049 1049 @LoginRequired()
1050 1050 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1051 1051 @view_config(
1052 1052 route_name='repo_files_edit_file', request_method='GET',
1053 1053 renderer='rhodecode:templates/files/files_edit.mako')
1054 1054 def repo_files_edit_file(self):
1055 1055 _ = self.request.translate
1056 1056 c = self.load_default_context()
1057 1057 commit_id, f_path = self._get_commit_and_path()
1058 1058
1059 1059 self._ensure_not_locked()
1060 1060
1061 1061 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1062 1062 h.flash(_('You can only edit files with commit '
1063 1063 'being a valid branch '), category='warning')
1064 1064 raise HTTPFound(
1065 1065 h.route_path('repo_files',
1066 1066 repo_name=self.db_repo_name, commit_id='tip',
1067 1067 f_path=f_path))
1068 1068
1069 1069 c.commit = self._get_commit_or_redirect(commit_id)
1070 1070 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1071 1071
1072 1072 if c.file.is_binary:
1073 1073 files_url = h.route_path(
1074 1074 'repo_files',
1075 1075 repo_name=self.db_repo_name,
1076 1076 commit_id=c.commit.raw_id, f_path=f_path)
1077 1077 raise HTTPFound(files_url)
1078 1078
1079 1079 c.default_message = _(
1080 1080 'Edited file {} via RhodeCode Enterprise').format(f_path)
1081 1081 c.f_path = f_path
1082 1082
1083 1083 return self._get_template_context(c)
1084 1084
1085 1085 @LoginRequired()
1086 1086 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1087 1087 @CSRFRequired()
1088 1088 @view_config(
1089 1089 route_name='repo_files_update_file', request_method='POST',
1090 1090 renderer=None)
1091 1091 def repo_files_update_file(self):
1092 1092 _ = self.request.translate
1093 1093 c = self.load_default_context()
1094 1094 commit_id, f_path = self._get_commit_and_path()
1095 1095
1096 1096 self._ensure_not_locked()
1097 1097
1098 1098 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1099 1099 h.flash(_('You can only edit files with commit '
1100 1100 'being a valid branch '), category='warning')
1101 1101 raise HTTPFound(
1102 1102 h.route_path('repo_files',
1103 1103 repo_name=self.db_repo_name, commit_id='tip',
1104 1104 f_path=f_path))
1105 1105
1106 1106 c.commit = self._get_commit_or_redirect(commit_id)
1107 1107 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1108 1108
1109 1109 if c.file.is_binary:
1110 1110 raise HTTPFound(
1111 1111 h.route_path('repo_files',
1112 1112 repo_name=self.db_repo_name,
1113 1113 commit_id=c.commit.raw_id,
1114 1114 f_path=f_path))
1115 1115
1116 1116 c.default_message = _(
1117 1117 'Edited file {} via RhodeCode Enterprise').format(f_path)
1118 1118 c.f_path = f_path
1119 1119 old_content = c.file.content
1120 1120 sl = old_content.splitlines(1)
1121 1121 first_line = sl[0] if sl else ''
1122 1122
1123 1123 r_post = self.request.POST
1124 1124 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1125 1125 mode = detect_mode(first_line, 0)
1126 1126 content = convert_line_endings(r_post.get('content', ''), mode)
1127 1127
1128 1128 message = r_post.get('message') or c.default_message
1129 1129 org_f_path = c.file.unicode_path
1130 1130 filename = r_post['filename']
1131 1131 org_filename = c.file.name
1132 1132
1133 1133 if content == old_content and filename == org_filename:
1134 1134 h.flash(_('No changes'), category='warning')
1135 1135 raise HTTPFound(
1136 h.route_path('changeset_home', repo_name=self.db_repo_name,
1137 revision='tip'))
1136 h.route_path('repo_commit', repo_name=self.db_repo_name,
1137 commit_id='tip'))
1138 1138 try:
1139 1139 mapping = {
1140 1140 org_f_path: {
1141 1141 'org_filename': org_f_path,
1142 1142 'filename': os.path.join(c.file.dir_path, filename),
1143 1143 'content': content,
1144 1144 'lexer': '',
1145 1145 'op': 'mod',
1146 1146 }
1147 1147 }
1148 1148
1149 1149 ScmModel().update_nodes(
1150 1150 user=self._rhodecode_db_user.user_id,
1151 1151 repo=self.db_repo,
1152 1152 message=message,
1153 1153 nodes=mapping,
1154 1154 parent_commit=c.commit,
1155 1155 )
1156 1156
1157 1157 h.flash(
1158 1158 _('Successfully committed changes to file `{}`').format(
1159 1159 h.escape(f_path)), category='success')
1160 1160 except Exception:
1161 1161 log.exception('Error occurred during commit')
1162 1162 h.flash(_('Error occurred during commit'), category='error')
1163 1163 raise HTTPFound(
1164 h.route_path('changeset_home', repo_name=self.db_repo_name,
1165 revision='tip'))
1164 h.route_path('repo_commit', repo_name=self.db_repo_name,
1165 commit_id='tip'))
1166 1166
1167 1167 @LoginRequired()
1168 1168 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1169 1169 @view_config(
1170 1170 route_name='repo_files_add_file', request_method='GET',
1171 1171 renderer='rhodecode:templates/files/files_add.mako')
1172 1172 def repo_files_add_file(self):
1173 1173 _ = self.request.translate
1174 1174 c = self.load_default_context()
1175 1175 commit_id, f_path = self._get_commit_and_path()
1176 1176
1177 1177 self._ensure_not_locked()
1178 1178
1179 1179 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1180 1180 if c.commit is None:
1181 1181 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1182 1182 c.default_message = (_('Added file via RhodeCode Enterprise'))
1183 1183 c.f_path = f_path
1184 1184
1185 1185 return self._get_template_context(c)
1186 1186
1187 1187 @LoginRequired()
1188 1188 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1189 1189 @CSRFRequired()
1190 1190 @view_config(
1191 1191 route_name='repo_files_create_file', request_method='POST',
1192 1192 renderer=None)
1193 1193 def repo_files_create_file(self):
1194 1194 _ = self.request.translate
1195 1195 c = self.load_default_context()
1196 1196 commit_id, f_path = self._get_commit_and_path()
1197 1197
1198 1198 self._ensure_not_locked()
1199 1199
1200 1200 r_post = self.request.POST
1201 1201
1202 1202 c.commit = self._get_commit_or_redirect(
1203 1203 commit_id, redirect_after=False)
1204 1204 if c.commit is None:
1205 1205 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1206 1206 c.default_message = (_('Added file via RhodeCode Enterprise'))
1207 1207 c.f_path = f_path
1208 1208 unix_mode = 0
1209 1209 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1210 1210
1211 1211 message = r_post.get('message') or c.default_message
1212 1212 filename = r_post.get('filename')
1213 1213 location = r_post.get('location', '') # dir location
1214 1214 file_obj = r_post.get('upload_file', None)
1215 1215
1216 1216 if file_obj is not None and hasattr(file_obj, 'filename'):
1217 1217 filename = r_post.get('filename_upload')
1218 1218 content = file_obj.file
1219 1219
1220 1220 if hasattr(content, 'file'):
1221 1221 # non posix systems store real file under file attr
1222 1222 content = content.file
1223 1223
1224 1224 default_redirect_url = h.route_path(
1225 'changeset_home', repo_name=self.db_repo_name, revision='tip')
1225 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1226 1226
1227 1227 # If there's no commit, redirect to repo summary
1228 1228 if type(c.commit) is EmptyCommit:
1229 1229 redirect_url = h.route_path(
1230 1230 'repo_summary', repo_name=self.db_repo_name)
1231 1231 else:
1232 1232 redirect_url = default_redirect_url
1233 1233
1234 1234 if not filename:
1235 1235 h.flash(_('No filename'), category='warning')
1236 1236 raise HTTPFound(redirect_url)
1237 1237
1238 1238 # extract the location from filename,
1239 1239 # allows using foo/bar.txt syntax to create subdirectories
1240 1240 subdir_loc = filename.rsplit('/', 1)
1241 1241 if len(subdir_loc) == 2:
1242 1242 location = os.path.join(location, subdir_loc[0])
1243 1243
1244 1244 # strip all crap out of file, just leave the basename
1245 1245 filename = os.path.basename(filename)
1246 1246 node_path = os.path.join(location, filename)
1247 1247 author = self._rhodecode_db_user.full_contact
1248 1248
1249 1249 try:
1250 1250 nodes = {
1251 1251 node_path: {
1252 1252 'content': content
1253 1253 }
1254 1254 }
1255 1255 ScmModel().create_nodes(
1256 1256 user=self._rhodecode_db_user.user_id,
1257 1257 repo=self.db_repo,
1258 1258 message=message,
1259 1259 nodes=nodes,
1260 1260 parent_commit=c.commit,
1261 1261 author=author,
1262 1262 )
1263 1263
1264 1264 h.flash(
1265 1265 _('Successfully committed new file `{}`').format(
1266 1266 h.escape(node_path)), category='success')
1267 1267 except NonRelativePathError:
1268 1268 h.flash(_(
1269 1269 'The location specified must be a relative path and must not '
1270 1270 'contain .. in the path'), category='warning')
1271 1271 raise HTTPFound(default_redirect_url)
1272 1272 except (NodeError, NodeAlreadyExistsError) as e:
1273 1273 h.flash(_(h.escape(e)), category='error')
1274 1274 except Exception:
1275 1275 log.exception('Error occurred during commit')
1276 1276 h.flash(_('Error occurred during commit'), category='error')
1277 1277
1278 1278 raise HTTPFound(default_redirect_url)
@@ -1,660 +1,599 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 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 class JSRoutesMapper(Mapper):
55 55 """
56 56 Wrapper for routes.Mapper to make pyroutes compatible url definitions
57 57 """
58 58 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
59 59 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
60 60 def __init__(self, *args, **kw):
61 61 super(JSRoutesMapper, self).__init__(*args, **kw)
62 62 self._jsroutes = []
63 63
64 64 def connect(self, *args, **kw):
65 65 """
66 66 Wrapper for connect to take an extra argument jsroute=True
67 67
68 68 :param jsroute: boolean, if True will add the route to the pyroutes list
69 69 """
70 70 if kw.pop('jsroute', False):
71 71 if not self._named_route_regex.match(args[0]):
72 72 raise Exception('only named routes can be added to pyroutes')
73 73 self._jsroutes.append(args[0])
74 74
75 75 super(JSRoutesMapper, self).connect(*args, **kw)
76 76
77 77 def _extract_route_information(self, route):
78 78 """
79 79 Convert a route into tuple(name, path, args), eg:
80 80 ('show_user', '/profile/%(username)s', ['username'])
81 81 """
82 82 routepath = route.routepath
83 83 def replace(matchobj):
84 84 if matchobj.group(1):
85 85 return "%%(%s)s" % matchobj.group(1).split(':')[0]
86 86 else:
87 87 return "%%(%s)s" % matchobj.group(2)
88 88
89 89 routepath = self._argument_prog.sub(replace, routepath)
90 90 return (
91 91 route.name,
92 92 routepath,
93 93 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
94 94 for arg in self._argument_prog.findall(route.routepath)]
95 95 )
96 96
97 97 def jsroutes(self):
98 98 """
99 99 Return a list of pyroutes.js compatible routes
100 100 """
101 101 for route_name in self._jsroutes:
102 102 yield self._extract_route_information(self._routenames[route_name])
103 103
104 104
105 105 def make_map(config):
106 106 """Create, configure and return the routes Mapper"""
107 107 rmap = JSRoutesMapper(
108 108 directory=config['pylons.paths']['controllers'],
109 109 always_scan=config['debug'])
110 110 rmap.minimization = False
111 111 rmap.explicit = False
112 112
113 113 from rhodecode.lib.utils2 import str2bool
114 114 from rhodecode.model import repo, repo_group
115 115
116 116 def check_repo(environ, match_dict):
117 117 """
118 118 check for valid repository for proper 404 handling
119 119
120 120 :param environ:
121 121 :param match_dict:
122 122 """
123 123 repo_name = match_dict.get('repo_name')
124 124
125 125 if match_dict.get('f_path'):
126 126 # fix for multiple initial slashes that causes errors
127 127 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
128 128 repo_model = repo.RepoModel()
129 129 by_name_match = repo_model.get_by_repo_name(repo_name)
130 130 # if we match quickly from database, short circuit the operation,
131 131 # and validate repo based on the type.
132 132 if by_name_match:
133 133 return True
134 134
135 135 by_id_match = repo_model.get_repo_by_id(repo_name)
136 136 if by_id_match:
137 137 repo_name = by_id_match.repo_name
138 138 match_dict['repo_name'] = repo_name
139 139 return True
140 140
141 141 return False
142 142
143 143 def check_group(environ, match_dict):
144 144 """
145 145 check for valid repository group path for proper 404 handling
146 146
147 147 :param environ:
148 148 :param match_dict:
149 149 """
150 150 repo_group_name = match_dict.get('group_name')
151 151 repo_group_model = repo_group.RepoGroupModel()
152 152 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
153 153 if by_name_match:
154 154 return True
155 155
156 156 return False
157 157
158 158 def check_user_group(environ, match_dict):
159 159 """
160 160 check for valid user group for proper 404 handling
161 161
162 162 :param environ:
163 163 :param match_dict:
164 164 """
165 165 return True
166 166
167 167 def check_int(environ, match_dict):
168 168 return match_dict.get('id').isdigit()
169 169
170 170
171 171 #==========================================================================
172 172 # CUSTOM ROUTES HERE
173 173 #==========================================================================
174 174
175 175 # ping and pylons error test
176 176 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
177 177 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
178 178
179 179 # ADMIN REPOSITORY ROUTES
180 180 with rmap.submapper(path_prefix=ADMIN_PREFIX,
181 181 controller='admin/repos') as m:
182 182 m.connect('repos', '/repos',
183 183 action='create', conditions={'method': ['POST']})
184 184 m.connect('repos', '/repos',
185 185 action='index', conditions={'method': ['GET']})
186 186 m.connect('new_repo', '/create_repository', jsroute=True,
187 187 action='create_repository', conditions={'method': ['GET']})
188 188 m.connect('delete_repo', '/repos/{repo_name}',
189 189 action='delete', conditions={'method': ['DELETE']},
190 190 requirements=URL_NAME_REQUIREMENTS)
191 191 m.connect('repo', '/repos/{repo_name}',
192 192 action='show', conditions={'method': ['GET'],
193 193 'function': check_repo},
194 194 requirements=URL_NAME_REQUIREMENTS)
195 195
196 196 # ADMIN REPOSITORY GROUPS ROUTES
197 197 with rmap.submapper(path_prefix=ADMIN_PREFIX,
198 198 controller='admin/repo_groups') as m:
199 199 m.connect('repo_groups', '/repo_groups',
200 200 action='create', conditions={'method': ['POST']})
201 201 m.connect('repo_groups', '/repo_groups',
202 202 action='index', conditions={'method': ['GET']})
203 203 m.connect('new_repo_group', '/repo_groups/new',
204 204 action='new', conditions={'method': ['GET']})
205 205 m.connect('update_repo_group', '/repo_groups/{group_name}',
206 206 action='update', conditions={'method': ['PUT'],
207 207 'function': check_group},
208 208 requirements=URL_NAME_REQUIREMENTS)
209 209
210 210 # EXTRAS REPO GROUP ROUTES
211 211 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
212 212 action='edit',
213 213 conditions={'method': ['GET'], 'function': check_group},
214 214 requirements=URL_NAME_REQUIREMENTS)
215 215 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
216 216 action='edit',
217 217 conditions={'method': ['PUT'], 'function': check_group},
218 218 requirements=URL_NAME_REQUIREMENTS)
219 219
220 220 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
221 221 action='edit_repo_group_advanced',
222 222 conditions={'method': ['GET'], 'function': check_group},
223 223 requirements=URL_NAME_REQUIREMENTS)
224 224 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
225 225 action='edit_repo_group_advanced',
226 226 conditions={'method': ['PUT'], 'function': check_group},
227 227 requirements=URL_NAME_REQUIREMENTS)
228 228
229 229 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
230 230 action='edit_repo_group_perms',
231 231 conditions={'method': ['GET'], 'function': check_group},
232 232 requirements=URL_NAME_REQUIREMENTS)
233 233 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
234 234 action='update_perms',
235 235 conditions={'method': ['PUT'], 'function': check_group},
236 236 requirements=URL_NAME_REQUIREMENTS)
237 237
238 238 m.connect('delete_repo_group', '/repo_groups/{group_name}',
239 239 action='delete', conditions={'method': ['DELETE'],
240 240 'function': check_group},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242
243 243 # ADMIN USER ROUTES
244 244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
245 245 controller='admin/users') as m:
246 246 m.connect('users', '/users',
247 247 action='create', conditions={'method': ['POST']})
248 248 m.connect('new_user', '/users/new',
249 249 action='new', conditions={'method': ['GET']})
250 250 m.connect('update_user', '/users/{user_id}',
251 251 action='update', conditions={'method': ['PUT']})
252 252 m.connect('delete_user', '/users/{user_id}',
253 253 action='delete', conditions={'method': ['DELETE']})
254 254 m.connect('edit_user', '/users/{user_id}/edit',
255 255 action='edit', conditions={'method': ['GET']}, jsroute=True)
256 256 m.connect('user', '/users/{user_id}',
257 257 action='show', conditions={'method': ['GET']})
258 258 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
259 259 action='reset_password', conditions={'method': ['POST']})
260 260 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
261 261 action='create_personal_repo_group', conditions={'method': ['POST']})
262 262
263 263 # EXTRAS USER ROUTES
264 264 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
265 265 action='edit_advanced', conditions={'method': ['GET']})
266 266 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
267 267 action='update_advanced', conditions={'method': ['PUT']})
268 268
269 269 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
270 270 action='edit_global_perms', conditions={'method': ['GET']})
271 271 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
272 272 action='update_global_perms', conditions={'method': ['PUT']})
273 273
274 274 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
275 275 action='edit_perms_summary', conditions={'method': ['GET']})
276 276
277 277 # ADMIN USER GROUPS REST ROUTES
278 278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
279 279 controller='admin/user_groups') as m:
280 280 m.connect('users_groups', '/user_groups',
281 281 action='create', conditions={'method': ['POST']})
282 282 m.connect('users_groups', '/user_groups',
283 283 action='index', conditions={'method': ['GET']})
284 284 m.connect('new_users_group', '/user_groups/new',
285 285 action='new', conditions={'method': ['GET']})
286 286 m.connect('update_users_group', '/user_groups/{user_group_id}',
287 287 action='update', conditions={'method': ['PUT']})
288 288 m.connect('delete_users_group', '/user_groups/{user_group_id}',
289 289 action='delete', conditions={'method': ['DELETE']})
290 290 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
291 291 action='edit', conditions={'method': ['GET']},
292 292 function=check_user_group)
293 293
294 294 # EXTRAS USER GROUP ROUTES
295 295 m.connect('edit_user_group_global_perms',
296 296 '/user_groups/{user_group_id}/edit/global_permissions',
297 297 action='edit_global_perms', conditions={'method': ['GET']})
298 298 m.connect('edit_user_group_global_perms',
299 299 '/user_groups/{user_group_id}/edit/global_permissions',
300 300 action='update_global_perms', conditions={'method': ['PUT']})
301 301 m.connect('edit_user_group_perms_summary',
302 302 '/user_groups/{user_group_id}/edit/permissions_summary',
303 303 action='edit_perms_summary', conditions={'method': ['GET']})
304 304
305 305 m.connect('edit_user_group_perms',
306 306 '/user_groups/{user_group_id}/edit/permissions',
307 307 action='edit_perms', conditions={'method': ['GET']})
308 308 m.connect('edit_user_group_perms',
309 309 '/user_groups/{user_group_id}/edit/permissions',
310 310 action='update_perms', conditions={'method': ['PUT']})
311 311
312 312 m.connect('edit_user_group_advanced',
313 313 '/user_groups/{user_group_id}/edit/advanced',
314 314 action='edit_advanced', conditions={'method': ['GET']})
315 315
316 316 m.connect('edit_user_group_advanced_sync',
317 317 '/user_groups/{user_group_id}/edit/advanced/sync',
318 318 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
319 319
320 320 m.connect('edit_user_group_members',
321 321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
322 322 action='user_group_members', conditions={'method': ['GET']})
323 323
324 324 # ADMIN DEFAULTS REST ROUTES
325 325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
326 326 controller='admin/defaults') as m:
327 327 m.connect('admin_defaults_repositories', '/defaults/repositories',
328 328 action='update_repository_defaults', conditions={'method': ['POST']})
329 329 m.connect('admin_defaults_repositories', '/defaults/repositories',
330 330 action='index', conditions={'method': ['GET']})
331 331
332 332 # ADMIN SETTINGS ROUTES
333 333 with rmap.submapper(path_prefix=ADMIN_PREFIX,
334 334 controller='admin/settings') as m:
335 335
336 336 # default
337 337 m.connect('admin_settings', '/settings',
338 338 action='settings_global_update',
339 339 conditions={'method': ['POST']})
340 340 m.connect('admin_settings', '/settings',
341 341 action='settings_global', conditions={'method': ['GET']})
342 342
343 343 m.connect('admin_settings_vcs', '/settings/vcs',
344 344 action='settings_vcs_update',
345 345 conditions={'method': ['POST']})
346 346 m.connect('admin_settings_vcs', '/settings/vcs',
347 347 action='settings_vcs',
348 348 conditions={'method': ['GET']})
349 349 m.connect('admin_settings_vcs', '/settings/vcs',
350 350 action='delete_svn_pattern',
351 351 conditions={'method': ['DELETE']})
352 352
353 353 m.connect('admin_settings_mapping', '/settings/mapping',
354 354 action='settings_mapping_update',
355 355 conditions={'method': ['POST']})
356 356 m.connect('admin_settings_mapping', '/settings/mapping',
357 357 action='settings_mapping', conditions={'method': ['GET']})
358 358
359 359 m.connect('admin_settings_global', '/settings/global',
360 360 action='settings_global_update',
361 361 conditions={'method': ['POST']})
362 362 m.connect('admin_settings_global', '/settings/global',
363 363 action='settings_global', conditions={'method': ['GET']})
364 364
365 365 m.connect('admin_settings_visual', '/settings/visual',
366 366 action='settings_visual_update',
367 367 conditions={'method': ['POST']})
368 368 m.connect('admin_settings_visual', '/settings/visual',
369 369 action='settings_visual', conditions={'method': ['GET']})
370 370
371 371 m.connect('admin_settings_issuetracker',
372 372 '/settings/issue-tracker', action='settings_issuetracker',
373 373 conditions={'method': ['GET']})
374 374 m.connect('admin_settings_issuetracker_save',
375 375 '/settings/issue-tracker/save',
376 376 action='settings_issuetracker_save',
377 377 conditions={'method': ['POST']})
378 378 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
379 379 action='settings_issuetracker_test',
380 380 conditions={'method': ['POST']})
381 381 m.connect('admin_issuetracker_delete',
382 382 '/settings/issue-tracker/delete',
383 383 action='settings_issuetracker_delete',
384 384 conditions={'method': ['DELETE']})
385 385
386 386 m.connect('admin_settings_email', '/settings/email',
387 387 action='settings_email_update',
388 388 conditions={'method': ['POST']})
389 389 m.connect('admin_settings_email', '/settings/email',
390 390 action='settings_email', conditions={'method': ['GET']})
391 391
392 392 m.connect('admin_settings_hooks', '/settings/hooks',
393 393 action='settings_hooks_update',
394 394 conditions={'method': ['POST', 'DELETE']})
395 395 m.connect('admin_settings_hooks', '/settings/hooks',
396 396 action='settings_hooks', conditions={'method': ['GET']})
397 397
398 398 m.connect('admin_settings_search', '/settings/search',
399 399 action='settings_search', conditions={'method': ['GET']})
400 400
401 401 m.connect('admin_settings_supervisor', '/settings/supervisor',
402 402 action='settings_supervisor', conditions={'method': ['GET']})
403 403 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
404 404 action='settings_supervisor_log', conditions={'method': ['GET']})
405 405
406 406 m.connect('admin_settings_labs', '/settings/labs',
407 407 action='settings_labs_update',
408 408 conditions={'method': ['POST']})
409 409 m.connect('admin_settings_labs', '/settings/labs',
410 410 action='settings_labs', conditions={'method': ['GET']})
411 411
412 412 # ADMIN MY ACCOUNT
413 413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 414 controller='admin/my_account') as m:
415 415
416 416 # NOTE(marcink): this needs to be kept for password force flag to be
417 417 # handled in pylons controllers, remove after full migration to pyramid
418 418 m.connect('my_account_password', '/my_account/password',
419 419 action='my_account_password', conditions={'method': ['GET']})
420 420
421 421 #==========================================================================
422 422 # REPOSITORY ROUTES
423 423 #==========================================================================
424 424
425 425 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
426 426 controller='admin/repos', action='repo_creating',
427 427 requirements=URL_NAME_REQUIREMENTS)
428 428 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
429 429 controller='admin/repos', action='repo_check',
430 430 requirements=URL_NAME_REQUIREMENTS)
431 431
432 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
433 controller='changeset', revision='tip',
434 conditions={'function': check_repo},
435 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
436 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
437 controller='changeset', revision='tip', action='changeset_children',
438 conditions={'function': check_repo},
439 requirements=URL_NAME_REQUIREMENTS)
440 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
441 controller='changeset', revision='tip', action='changeset_parents',
442 conditions={'function': check_repo},
443 requirements=URL_NAME_REQUIREMENTS)
444
445 432 # repo edit options
446 433 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
447 434 controller='admin/repos', action='edit_fields',
448 435 conditions={'method': ['GET'], 'function': check_repo},
449 436 requirements=URL_NAME_REQUIREMENTS)
450 437 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
451 438 controller='admin/repos', action='create_repo_field',
452 439 conditions={'method': ['PUT'], 'function': check_repo},
453 440 requirements=URL_NAME_REQUIREMENTS)
454 441 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
455 442 controller='admin/repos', action='delete_repo_field',
456 443 conditions={'method': ['DELETE'], 'function': check_repo},
457 444 requirements=URL_NAME_REQUIREMENTS)
458 445
459 446 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
460 447 controller='admin/repos', action='toggle_locking',
461 448 conditions={'method': ['GET'], 'function': check_repo},
462 449 requirements=URL_NAME_REQUIREMENTS)
463 450
464 451 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
465 452 controller='admin/repos', action='edit_remote_form',
466 453 conditions={'method': ['GET'], 'function': check_repo},
467 454 requirements=URL_NAME_REQUIREMENTS)
468 455 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
469 456 controller='admin/repos', action='edit_remote',
470 457 conditions={'method': ['PUT'], 'function': check_repo},
471 458 requirements=URL_NAME_REQUIREMENTS)
472 459
473 460 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
474 461 controller='admin/repos', action='edit_statistics_form',
475 462 conditions={'method': ['GET'], 'function': check_repo},
476 463 requirements=URL_NAME_REQUIREMENTS)
477 464 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
478 465 controller='admin/repos', action='edit_statistics',
479 466 conditions={'method': ['PUT'], 'function': check_repo},
480 467 requirements=URL_NAME_REQUIREMENTS)
481 468 rmap.connect('repo_settings_issuetracker',
482 469 '/{repo_name}/settings/issue-tracker',
483 470 controller='admin/repos', action='repo_issuetracker',
484 471 conditions={'method': ['GET'], 'function': check_repo},
485 472 requirements=URL_NAME_REQUIREMENTS)
486 473 rmap.connect('repo_issuetracker_test',
487 474 '/{repo_name}/settings/issue-tracker/test',
488 475 controller='admin/repos', action='repo_issuetracker_test',
489 476 conditions={'method': ['POST'], 'function': check_repo},
490 477 requirements=URL_NAME_REQUIREMENTS)
491 478 rmap.connect('repo_issuetracker_delete',
492 479 '/{repo_name}/settings/issue-tracker/delete',
493 480 controller='admin/repos', action='repo_issuetracker_delete',
494 481 conditions={'method': ['DELETE'], 'function': check_repo},
495 482 requirements=URL_NAME_REQUIREMENTS)
496 483 rmap.connect('repo_issuetracker_save',
497 484 '/{repo_name}/settings/issue-tracker/save',
498 485 controller='admin/repos', action='repo_issuetracker_save',
499 486 conditions={'method': ['POST'], 'function': check_repo},
500 487 requirements=URL_NAME_REQUIREMENTS)
501 488 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
502 489 controller='admin/repos', action='repo_settings_vcs_update',
503 490 conditions={'method': ['POST'], 'function': check_repo},
504 491 requirements=URL_NAME_REQUIREMENTS)
505 492 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
506 493 controller='admin/repos', action='repo_settings_vcs',
507 494 conditions={'method': ['GET'], 'function': check_repo},
508 495 requirements=URL_NAME_REQUIREMENTS)
509 496 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
510 497 controller='admin/repos', action='repo_delete_svn_pattern',
511 498 conditions={'method': ['DELETE'], 'function': check_repo},
512 499 requirements=URL_NAME_REQUIREMENTS)
513 500 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
514 501 controller='admin/repos', action='repo_settings_pullrequest',
515 502 conditions={'method': ['GET', 'POST'], 'function': check_repo},
516 503 requirements=URL_NAME_REQUIREMENTS)
517 504
518 # still working url for backward compat.
519 rmap.connect('raw_changeset_home_depraced',
520 '/{repo_name}/raw-changeset/{revision}',
521 controller='changeset', action='changeset_raw',
522 revision='tip', conditions={'function': check_repo},
523 requirements=URL_NAME_REQUIREMENTS)
524
525 # new URLs
526 rmap.connect('changeset_raw_home',
527 '/{repo_name}/changeset-diff/{revision}',
528 controller='changeset', action='changeset_raw',
529 revision='tip', conditions={'function': check_repo},
530 requirements=URL_NAME_REQUIREMENTS)
531
532 rmap.connect('changeset_patch_home',
533 '/{repo_name}/changeset-patch/{revision}',
534 controller='changeset', action='changeset_patch',
535 revision='tip', conditions={'function': check_repo},
536 requirements=URL_NAME_REQUIREMENTS)
537
538 rmap.connect('changeset_download_home',
539 '/{repo_name}/changeset-download/{revision}',
540 controller='changeset', action='changeset_download',
541 revision='tip', conditions={'function': check_repo},
542 requirements=URL_NAME_REQUIREMENTS)
543
544 rmap.connect('changeset_comment',
545 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
546 controller='changeset', revision='tip', action='comment',
547 conditions={'function': check_repo},
548 requirements=URL_NAME_REQUIREMENTS)
549
550 rmap.connect('changeset_comment_preview',
551 '/{repo_name}/changeset/comment/preview', jsroute=True,
552 controller='changeset', action='preview_comment',
553 conditions={'function': check_repo, 'method': ['POST']},
554 requirements=URL_NAME_REQUIREMENTS)
555
556 rmap.connect('changeset_comment_delete',
557 '/{repo_name}/changeset/comment/{comment_id}/delete',
558 controller='changeset', action='delete_comment',
559 conditions={'function': check_repo, 'method': ['DELETE']},
560 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
561
562 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
563 controller='changeset', action='changeset_info',
564 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
565
566 505 rmap.connect('compare_home',
567 506 '/{repo_name}/compare',
568 507 controller='compare', action='index',
569 508 conditions={'function': check_repo},
570 509 requirements=URL_NAME_REQUIREMENTS)
571 510
572 511 rmap.connect('compare_url',
573 512 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
574 513 controller='compare', action='compare',
575 514 conditions={'function': check_repo},
576 515 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
577 516
578 517 rmap.connect('pullrequest_home',
579 518 '/{repo_name}/pull-request/new', controller='pullrequests',
580 519 action='index', conditions={'function': check_repo,
581 520 'method': ['GET']},
582 521 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
583 522
584 523 rmap.connect('pullrequest',
585 524 '/{repo_name}/pull-request/new', controller='pullrequests',
586 525 action='create', conditions={'function': check_repo,
587 526 'method': ['POST']},
588 527 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
589 528
590 529 rmap.connect('pullrequest_repo_refs',
591 530 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
592 531 controller='pullrequests',
593 532 action='get_repo_refs',
594 533 conditions={'function': check_repo, 'method': ['GET']},
595 534 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
596 535
597 536 rmap.connect('pullrequest_repo_destinations',
598 537 '/{repo_name}/pull-request/repo-destinations',
599 538 controller='pullrequests',
600 539 action='get_repo_destinations',
601 540 conditions={'function': check_repo, 'method': ['GET']},
602 541 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
603 542
604 543 rmap.connect('pullrequest_show',
605 544 '/{repo_name}/pull-request/{pull_request_id}',
606 545 controller='pullrequests',
607 546 action='show', conditions={'function': check_repo,
608 547 'method': ['GET']},
609 548 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
610 549
611 550 rmap.connect('pullrequest_update',
612 551 '/{repo_name}/pull-request/{pull_request_id}',
613 552 controller='pullrequests',
614 553 action='update', conditions={'function': check_repo,
615 554 'method': ['PUT']},
616 555 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
617 556
618 557 rmap.connect('pullrequest_merge',
619 558 '/{repo_name}/pull-request/{pull_request_id}',
620 559 controller='pullrequests',
621 560 action='merge', conditions={'function': check_repo,
622 561 'method': ['POST']},
623 562 requirements=URL_NAME_REQUIREMENTS)
624 563
625 564 rmap.connect('pullrequest_delete',
626 565 '/{repo_name}/pull-request/{pull_request_id}',
627 566 controller='pullrequests',
628 567 action='delete', conditions={'function': check_repo,
629 568 'method': ['DELETE']},
630 569 requirements=URL_NAME_REQUIREMENTS)
631 570
632 571 rmap.connect('pullrequest_comment',
633 572 '/{repo_name}/pull-request-comment/{pull_request_id}',
634 573 controller='pullrequests',
635 574 action='comment', conditions={'function': check_repo,
636 575 'method': ['POST']},
637 576 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
638 577
639 578 rmap.connect('pullrequest_comment_delete',
640 579 '/{repo_name}/pull-request-comment/{comment_id}/delete',
641 580 controller='pullrequests', action='delete_comment',
642 581 conditions={'function': check_repo, 'method': ['DELETE']},
643 582 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
644 583
645 584 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
646 585 controller='forks', action='fork_create',
647 586 conditions={'function': check_repo, 'method': ['POST']},
648 587 requirements=URL_NAME_REQUIREMENTS)
649 588
650 589 rmap.connect('repo_fork_home', '/{repo_name}/fork',
651 590 controller='forks', action='fork',
652 591 conditions={'function': check_repo},
653 592 requirements=URL_NAME_REQUIREMENTS)
654 593
655 594 rmap.connect('repo_forks_home', '/{repo_name}/forks',
656 595 controller='forks', action='forks',
657 596 conditions={'function': check_repo},
658 597 requirements=URL_NAME_REQUIREMENTS)
659 598
660 599 return rmap
@@ -1,349 +1,351 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 import logging
22 22
23 from pylons import url
24 23 from pylons.i18n.translation import _
25 24 from webhelpers.html.builder import literal
26 25 from webhelpers.html.tags import link_to
27 26
28 27 from rhodecode.lib.utils2 import AttributeDict
29 28 from rhodecode.lib.vcs.backends.base import BaseCommit
30 29 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
31 30
32 31
33 32 log = logging.getLogger(__name__)
34 33
35 34
36 35 def action_parser(user_log, feed=False, parse_cs=False):
37 36 """
38 37 This helper will action_map the specified string action into translated
39 38 fancy names with icons and links
40 39
41 40 :param user_log: user log instance
42 41 :param feed: use output for feeds (no html and fancy icons)
43 42 :param parse_cs: parse Changesets into VCS instances
44 43 """
45 44 if user_log.version == 'v2':
46 45 ap = AuditLogParser(user_log)
47 46 return ap.callbacks()
48 47 else:
49 48 # old style
50 49 ap = ActionParser(user_log, feed=False, parse_commits=False)
51 50 return ap.callbacks()
52 51
53 52
54 53 class ActionParser(object):
55 54
56 55 commits_limit = 3 # display this amount always
57 56 commits_top_limit = 50 # show up to this amount of commits hidden
58 57
59 58 def __init__(self, user_log, feed=False, parse_commits=False):
60 59 self.user_log = user_log
61 60 self.feed = feed
62 61 self.parse_commits = parse_commits
63 62
64 63 self.action = user_log.action
65 64 self.action_params = ' '
66 65 x = self.action.split(':', 1)
67 66 if len(x) > 1:
68 67 self.action, self.action_params = x
69 68
70 69 def callbacks(self):
71 70 action_str = self.action_map.get(self.action, self.action)
72 71 if self.feed:
73 72 action = action_str[0].replace('[', '').replace(']', '')
74 73 else:
75 74 action = action_str[0]\
76 75 .replace('[', '<span class="journal_highlight">')\
77 76 .replace(']', '</span>')
78 77
79 78 action_params_func = _no_params_func
80 79 if callable(action_str[1]):
81 80 action_params_func = action_str[1]
82 81
83 82 # returned callbacks we need to call to get
84 83 return [
85 84 lambda: literal(action), action_params_func,
86 85 self.action_parser_icon]
87 86
88 87 @property
89 88 def action_map(self):
90 89
91 90 # action : translated str, callback(extractor), icon
92 91 action_map = {
93 92 'user_deleted_repo': (
94 93 _('[deleted] repository'),
95 94 None, 'icon-trash'),
96 95 'user_created_repo': (
97 96 _('[created] repository'),
98 97 None, 'icon-plus icon-plus-colored'),
99 98 'user_created_fork': (
100 99 _('[created] repository as fork'),
101 100 None, 'icon-code-fork'),
102 101 'user_forked_repo': (
103 102 _('[forked] repository'),
104 103 self.get_fork_name, 'icon-code-fork'),
105 104 'user_updated_repo': (
106 105 _('[updated] repository'),
107 106 None, 'icon-pencil icon-pencil-colored'),
108 107 'user_downloaded_archive': (
109 108 _('[downloaded] archive from repository'),
110 109 self.get_archive_name, 'icon-download-alt'),
111 110 'admin_deleted_repo': (
112 111 _('[delete] repository'),
113 112 None, 'icon-trash'),
114 113 'admin_created_repo': (
115 114 _('[created] repository'),
116 115 None, 'icon-plus icon-plus-colored'),
117 116 'admin_forked_repo': (
118 117 _('[forked] repository'),
119 118 None, 'icon-code-fork icon-fork-colored'),
120 119 'admin_updated_repo': (
121 120 _('[updated] repository'),
122 121 None, 'icon-pencil icon-pencil-colored'),
123 122 'admin_created_user': (
124 123 _('[created] user'),
125 124 self.get_user_name, 'icon-user icon-user-colored'),
126 125 'admin_updated_user': (
127 126 _('[updated] user'),
128 127 self.get_user_name, 'icon-user icon-user-colored'),
129 128 'admin_created_users_group': (
130 129 _('[created] user group'),
131 130 self.get_users_group, 'icon-pencil icon-pencil-colored'),
132 131 'admin_updated_users_group': (
133 132 _('[updated] user group'),
134 133 self.get_users_group, 'icon-pencil icon-pencil-colored'),
135 134 'user_commented_revision': (
136 135 _('[commented] on commit in repository'),
137 136 self.get_cs_links, 'icon-comment icon-comment-colored'),
138 137 'user_commented_pull_request': (
139 138 _('[commented] on pull request for'),
140 139 self.get_pull_request, 'icon-comment icon-comment-colored'),
141 140 'user_closed_pull_request': (
142 141 _('[closed] pull request for'),
143 142 self.get_pull_request, 'icon-check'),
144 143 'user_merged_pull_request': (
145 144 _('[merged] pull request for'),
146 145 self.get_pull_request, 'icon-check'),
147 146 'push': (
148 147 _('[pushed] into'),
149 148 self.get_cs_links, 'icon-arrow-up'),
150 149 'push_local': (
151 150 _('[committed via RhodeCode] into repository'),
152 151 self.get_cs_links, 'icon-pencil icon-pencil-colored'),
153 152 'push_remote': (
154 153 _('[pulled from remote] into repository'),
155 154 self.get_cs_links, 'icon-arrow-up'),
156 155 'pull': (
157 156 _('[pulled] from'),
158 157 None, 'icon-arrow-down'),
159 158 'started_following_repo': (
160 159 _('[started following] repository'),
161 160 None, 'icon-heart icon-heart-colored'),
162 161 'stopped_following_repo': (
163 162 _('[stopped following] repository'),
164 163 None, 'icon-heart-empty icon-heart-colored'),
165 164 }
166 165 return action_map
167 166
168 167 def get_fork_name(self):
169 168 from rhodecode.lib import helpers as h
170 169 repo_name = self.action_params
171 170 _url = h.route_path('repo_summary', repo_name=repo_name)
172 171 return _('fork name %s') % link_to(self.action_params, _url)
173 172
174 173 def get_user_name(self):
175 174 user_name = self.action_params
176 175 return user_name
177 176
178 177 def get_users_group(self):
179 178 group_name = self.action_params
180 179 return group_name
181 180
182 181 def get_pull_request(self):
183 182 from rhodecode.lib import helpers as h
184 183 pull_request_id = self.action_params
185 184 if self.is_deleted():
186 185 repo_name = self.user_log.repository_name
187 186 else:
188 187 repo_name = self.user_log.repository.repo_name
189 188 return link_to(
190 189 _('Pull request #%s') % pull_request_id,
191 190 h.route_path('pullrequest_show', repo_name=repo_name,
192 191 pull_request_id=pull_request_id))
193 192
194 193 def get_archive_name(self):
195 194 archive_name = self.action_params
196 195 return archive_name
197 196
198 197 def action_parser_icon(self):
199 198 tmpl = """<i class="%s" alt="%s"></i>"""
200 199 ico = self.action_map.get(self.action, ['', '', ''])[2]
201 200 return literal(tmpl % (ico, self.action))
202 201
203 202 def get_cs_links(self):
203 from rhodecode.lib import helpers as h
204 204 if self.is_deleted():
205 205 return self.action_params
206 206
207 207 repo_name = self.user_log.repository.repo_name
208 208 commit_ids = self.action_params.split(',')
209 209 commits = self.get_commits(commit_ids)
210 210
211 211 link_generator = (
212 212 self.lnk(commit, repo_name)
213 213 for commit in commits[:self.commits_limit])
214 214 commit_links = [" " + ', '.join(link_generator)]
215 215 _op1, _name1 = _get_op(commit_ids[0])
216 216 _op2, _name2 = _get_op(commit_ids[-1])
217 217
218 218 commit_id_range = '%s...%s' % (_name1, _name2)
219 219
220 220 compare_view = (
221 221 ' <div class="compare_view tooltip" title="%s">'
222 222 '<a href="%s">%s</a> </div>' % (
223 223 _('Show all combined commits %s->%s') % (
224 224 commit_ids[0][:12], commit_ids[-1][:12]
225 225 ),
226 url('changeset_home', repo_name=repo_name,
227 revision=commit_id_range), _('compare view')
226 h.route_path(
227 'repo_commit', repo_name=repo_name,
228 commit_id=commit_id_range), _('compare view')
228 229 )
229 230 )
230 231
231 232 if len(commit_ids) > self.commits_limit:
232 233 more_count = len(commit_ids) - self.commits_limit
233 234 commit_links.append(
234 235 _(' and %(num)s more commits') % {'num': more_count}
235 236 )
236 237
237 238 if len(commits) > 1:
238 239 commit_links.append(compare_view)
239 240 return ''.join(commit_links)
240 241
241 242 def get_commits(self, commit_ids):
242 243 commits = []
243 244 if not filter(lambda v: v != '', commit_ids):
244 245 return commits
245 246
246 247 repo = None
247 248 if self.parse_commits:
248 249 repo = self.user_log.repository.scm_instance()
249 250
250 251 for commit_id in commit_ids[:self.commits_top_limit]:
251 252 _op, _name = _get_op(commit_id)
252 253
253 254 # we want parsed commits, or new log store format is bad
254 255 if self.parse_commits:
255 256 try:
256 257 commit = repo.get_commit(commit_id=commit_id)
257 258 commits.append(commit)
258 259 except CommitDoesNotExistError:
259 260 log.error(
260 261 'cannot find commit id %s in this repository',
261 262 commit_id)
262 263 commits.append(commit_id)
263 264 continue
264 265 else:
265 266 fake_commit = AttributeDict({
266 267 'short_id': commit_id[:12],
267 268 'raw_id': commit_id,
268 269 'message': '',
269 270 'op': _op,
270 271 'ref_name': _name
271 272 })
272 273 commits.append(fake_commit)
273 274
274 275 return commits
275 276
276 277 def lnk(self, commit_or_id, repo_name):
277 278 from rhodecode.lib.helpers import tooltip
279 from rhodecode.lib import helpers as h
278 280
279 281 if isinstance(commit_or_id, (BaseCommit, AttributeDict)):
280 282 lazy_cs = True
281 283 if (getattr(commit_or_id, 'op', None) and
282 284 getattr(commit_or_id, 'ref_name', None)):
283 285 lazy_cs = False
284 286 lbl = '?'
285 287 if commit_or_id.op == 'delete_branch':
286 288 lbl = '%s' % _('Deleted branch: %s') % commit_or_id.ref_name
287 289 title = ''
288 290 elif commit_or_id.op == 'tag':
289 291 lbl = '%s' % _('Created tag: %s') % commit_or_id.ref_name
290 292 title = ''
291 293 _url = '#'
292 294
293 295 else:
294 296 lbl = '%s' % (commit_or_id.short_id[:8])
295 _url = url('changeset_home', repo_name=repo_name,
296 revision=commit_or_id.raw_id)
297 _url = h.route_path('repo_commit', repo_name=repo_name,
298 commit_id=commit_or_id.raw_id)
297 299 title = tooltip(commit_or_id.message)
298 300 else:
299 301 # commit cannot be found/striped/removed etc.
300 302 lbl = ('%s' % commit_or_id)[:12]
301 303 _url = '#'
302 304 title = _('Commit not found')
303 305 if self.parse_commits:
304 306 return link_to(lbl, _url, title=title, class_='tooltip')
305 307 return link_to(lbl, _url, raw_id=commit_or_id.raw_id, repo_name=repo_name,
306 308 class_='lazy-cs' if lazy_cs else '')
307 309
308 310 def is_deleted(self):
309 311 return self.user_log.repository is None
310 312
311 313
312 314 class AuditLogParser(object):
313 315 def __init__(self, audit_log_entry):
314 316 self.audit_log_entry = audit_log_entry
315 317
316 318 def get_icon(self, action):
317 319 return 'icon-rhodecode'
318 320
319 321 def callbacks(self):
320 322 action_str = self.audit_log_entry.action
321 323
322 324 def callback():
323 325 # returned callbacks we need to call to get
324 326 action = action_str \
325 327 .replace('[', '<span class="journal_highlight">')\
326 328 .replace(']', '</span>')
327 329 return literal(action)
328 330
329 331 def icon():
330 332 tmpl = """<i class="%s" alt="%s"></i>"""
331 333 ico = self.get_icon(action_str)
332 334 return literal(tmpl % (ico, action_str))
333 335
334 336 action_params_func = _no_params_func
335 337
336 338 return [
337 339 callback, action_params_func, icon]
338 340
339 341
340 342 def _no_params_func():
341 343 return ""
342 344
343 345
344 346 def _get_op(commit_id):
345 347 _op = None
346 348 _name = commit_id
347 349 if len(commit_id.split('=>')) == 2:
348 350 _op, _name = commit_id.split('=>')
349 351 return _op, _name
@@ -1,2027 +1,2027 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 authentication and permission libraries
23 23 """
24 24
25 25 import os
26 26 import inspect
27 27 import collections
28 28 import fnmatch
29 29 import hashlib
30 30 import itertools
31 31 import logging
32 32 import random
33 33 import traceback
34 34 from functools import wraps
35 35
36 36 import ipaddress
37 37 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
38 38 from pylons.i18n.translation import _
39 39 # NOTE(marcink): this has to be removed only after pyramid migration,
40 40 # replace with _ = request.translate
41 41 from sqlalchemy.orm.exc import ObjectDeletedError
42 42 from sqlalchemy.orm import joinedload
43 43 from zope.cachedescriptors.property import Lazy as LazyProperty
44 44
45 45 import rhodecode
46 46 from rhodecode.model import meta
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.db import (
50 50 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
51 51 UserIpMap, UserApiKeys, RepoGroup)
52 52 from rhodecode.lib import caches
53 53 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
54 54 from rhodecode.lib.utils import (
55 55 get_repo_slug, get_repo_group_slug, get_user_group_slug)
56 56 from rhodecode.lib.caching_query import FromCache
57 57
58 58
59 59 if rhodecode.is_unix:
60 60 import bcrypt
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 csrf_token_key = "csrf_token"
65 65
66 66
67 67 class PasswordGenerator(object):
68 68 """
69 69 This is a simple class for generating password from different sets of
70 70 characters
71 71 usage::
72 72
73 73 passwd_gen = PasswordGenerator()
74 74 #print 8-letter password containing only big and small letters
75 75 of alphabet
76 76 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
77 77 """
78 78 ALPHABETS_NUM = r'''1234567890'''
79 79 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
80 80 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
81 81 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
82 82 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
83 83 + ALPHABETS_NUM + ALPHABETS_SPECIAL
84 84 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
85 85 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
86 86 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
87 87 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
88 88
89 89 def __init__(self, passwd=''):
90 90 self.passwd = passwd
91 91
92 92 def gen_password(self, length, type_=None):
93 93 if type_ is None:
94 94 type_ = self.ALPHABETS_FULL
95 95 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
96 96 return self.passwd
97 97
98 98
99 99 class _RhodeCodeCryptoBase(object):
100 100 ENC_PREF = None
101 101
102 102 def hash_create(self, str_):
103 103 """
104 104 hash the string using
105 105
106 106 :param str_: password to hash
107 107 """
108 108 raise NotImplementedError
109 109
110 110 def hash_check_with_upgrade(self, password, hashed):
111 111 """
112 112 Returns tuple in which first element is boolean that states that
113 113 given password matches it's hashed version, and the second is new hash
114 114 of the password, in case this password should be migrated to new
115 115 cipher.
116 116 """
117 117 checked_hash = self.hash_check(password, hashed)
118 118 return checked_hash, None
119 119
120 120 def hash_check(self, password, hashed):
121 121 """
122 122 Checks matching password with it's hashed value.
123 123
124 124 :param password: password
125 125 :param hashed: password in hashed form
126 126 """
127 127 raise NotImplementedError
128 128
129 129 def _assert_bytes(self, value):
130 130 """
131 131 Passing in an `unicode` object can lead to hard to detect issues
132 132 if passwords contain non-ascii characters. Doing a type check
133 133 during runtime, so that such mistakes are detected early on.
134 134 """
135 135 if not isinstance(value, str):
136 136 raise TypeError(
137 137 "Bytestring required as input, got %r." % (value, ))
138 138
139 139
140 140 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
141 141 ENC_PREF = ('$2a$10', '$2b$10')
142 142
143 143 def hash_create(self, str_):
144 144 self._assert_bytes(str_)
145 145 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
146 146
147 147 def hash_check_with_upgrade(self, password, hashed):
148 148 """
149 149 Returns tuple in which first element is boolean that states that
150 150 given password matches it's hashed version, and the second is new hash
151 151 of the password, in case this password should be migrated to new
152 152 cipher.
153 153
154 154 This implements special upgrade logic which works like that:
155 155 - check if the given password == bcrypted hash, if yes then we
156 156 properly used password and it was already in bcrypt. Proceed
157 157 without any changes
158 158 - if bcrypt hash check is not working try with sha256. If hash compare
159 159 is ok, it means we using correct but old hashed password. indicate
160 160 hash change and proceed
161 161 """
162 162
163 163 new_hash = None
164 164
165 165 # regular pw check
166 166 password_match_bcrypt = self.hash_check(password, hashed)
167 167
168 168 # now we want to know if the password was maybe from sha256
169 169 # basically calling _RhodeCodeCryptoSha256().hash_check()
170 170 if not password_match_bcrypt:
171 171 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
172 172 new_hash = self.hash_create(password) # make new bcrypt hash
173 173 password_match_bcrypt = True
174 174
175 175 return password_match_bcrypt, new_hash
176 176
177 177 def hash_check(self, password, hashed):
178 178 """
179 179 Checks matching password with it's hashed value.
180 180
181 181 :param password: password
182 182 :param hashed: password in hashed form
183 183 """
184 184 self._assert_bytes(password)
185 185 try:
186 186 return bcrypt.hashpw(password, hashed) == hashed
187 187 except ValueError as e:
188 188 # we're having a invalid salt here probably, we should not crash
189 189 # just return with False as it would be a wrong password.
190 190 log.debug('Failed to check password hash using bcrypt %s',
191 191 safe_str(e))
192 192
193 193 return False
194 194
195 195
196 196 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
197 197 ENC_PREF = '_'
198 198
199 199 def hash_create(self, str_):
200 200 self._assert_bytes(str_)
201 201 return hashlib.sha256(str_).hexdigest()
202 202
203 203 def hash_check(self, password, hashed):
204 204 """
205 205 Checks matching password with it's hashed value.
206 206
207 207 :param password: password
208 208 :param hashed: password in hashed form
209 209 """
210 210 self._assert_bytes(password)
211 211 return hashlib.sha256(password).hexdigest() == hashed
212 212
213 213
214 214 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
215 215 ENC_PREF = '_'
216 216
217 217 def hash_create(self, str_):
218 218 self._assert_bytes(str_)
219 219 return hashlib.md5(str_).hexdigest()
220 220
221 221 def hash_check(self, password, hashed):
222 222 """
223 223 Checks matching password with it's hashed value.
224 224
225 225 :param password: password
226 226 :param hashed: password in hashed form
227 227 """
228 228 self._assert_bytes(password)
229 229 return hashlib.md5(password).hexdigest() == hashed
230 230
231 231
232 232 def crypto_backend():
233 233 """
234 234 Return the matching crypto backend.
235 235
236 236 Selection is based on if we run tests or not, we pick md5 backend to run
237 237 tests faster since BCRYPT is expensive to calculate
238 238 """
239 239 if rhodecode.is_test:
240 240 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
241 241 else:
242 242 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
243 243
244 244 return RhodeCodeCrypto
245 245
246 246
247 247 def get_crypt_password(password):
248 248 """
249 249 Create the hash of `password` with the active crypto backend.
250 250
251 251 :param password: The cleartext password.
252 252 :type password: unicode
253 253 """
254 254 password = safe_str(password)
255 255 return crypto_backend().hash_create(password)
256 256
257 257
258 258 def check_password(password, hashed):
259 259 """
260 260 Check if the value in `password` matches the hash in `hashed`.
261 261
262 262 :param password: The cleartext password.
263 263 :type password: unicode
264 264
265 265 :param hashed: The expected hashed version of the password.
266 266 :type hashed: The hash has to be passed in in text representation.
267 267 """
268 268 password = safe_str(password)
269 269 return crypto_backend().hash_check(password, hashed)
270 270
271 271
272 272 def generate_auth_token(data, salt=None):
273 273 """
274 274 Generates API KEY from given string
275 275 """
276 276
277 277 if salt is None:
278 278 salt = os.urandom(16)
279 279 return hashlib.sha1(safe_str(data) + salt).hexdigest()
280 280
281 281
282 282 class CookieStoreWrapper(object):
283 283
284 284 def __init__(self, cookie_store):
285 285 self.cookie_store = cookie_store
286 286
287 287 def __repr__(self):
288 288 return 'CookieStore<%s>' % (self.cookie_store)
289 289
290 290 def get(self, key, other=None):
291 291 if isinstance(self.cookie_store, dict):
292 292 return self.cookie_store.get(key, other)
293 293 elif isinstance(self.cookie_store, AuthUser):
294 294 return self.cookie_store.__dict__.get(key, other)
295 295
296 296
297 297 def _cached_perms_data(user_id, scope, user_is_admin,
298 298 user_inherit_default_permissions, explicit, algo):
299 299
300 300 permissions = PermissionCalculator(
301 301 user_id, scope, user_is_admin, user_inherit_default_permissions,
302 302 explicit, algo)
303 303 return permissions.calculate()
304 304
305 305
306 306 class PermOrigin(object):
307 307 ADMIN = 'superadmin'
308 308
309 309 REPO_USER = 'user:%s'
310 310 REPO_USERGROUP = 'usergroup:%s'
311 311 REPO_OWNER = 'repo.owner'
312 312 REPO_DEFAULT = 'repo.default'
313 313 REPO_PRIVATE = 'repo.private'
314 314
315 315 REPOGROUP_USER = 'user:%s'
316 316 REPOGROUP_USERGROUP = 'usergroup:%s'
317 317 REPOGROUP_OWNER = 'group.owner'
318 318 REPOGROUP_DEFAULT = 'group.default'
319 319
320 320 USERGROUP_USER = 'user:%s'
321 321 USERGROUP_USERGROUP = 'usergroup:%s'
322 322 USERGROUP_OWNER = 'usergroup.owner'
323 323 USERGROUP_DEFAULT = 'usergroup.default'
324 324
325 325
326 326 class PermOriginDict(dict):
327 327 """
328 328 A special dict used for tracking permissions along with their origins.
329 329
330 330 `__setitem__` has been overridden to expect a tuple(perm, origin)
331 331 `__getitem__` will return only the perm
332 332 `.perm_origin_stack` will return the stack of (perm, origin) set per key
333 333
334 334 >>> perms = PermOriginDict()
335 335 >>> perms['resource'] = 'read', 'default'
336 336 >>> perms['resource']
337 337 'read'
338 338 >>> perms['resource'] = 'write', 'admin'
339 339 >>> perms['resource']
340 340 'write'
341 341 >>> perms.perm_origin_stack
342 342 {'resource': [('read', 'default'), ('write', 'admin')]}
343 343 """
344 344
345 345 def __init__(self, *args, **kw):
346 346 dict.__init__(self, *args, **kw)
347 347 self.perm_origin_stack = {}
348 348
349 349 def __setitem__(self, key, (perm, origin)):
350 350 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
351 351 dict.__setitem__(self, key, perm)
352 352
353 353
354 354 class PermissionCalculator(object):
355 355
356 356 def __init__(
357 357 self, user_id, scope, user_is_admin,
358 358 user_inherit_default_permissions, explicit, algo):
359 359 self.user_id = user_id
360 360 self.user_is_admin = user_is_admin
361 361 self.inherit_default_permissions = user_inherit_default_permissions
362 362 self.explicit = explicit
363 363 self.algo = algo
364 364
365 365 scope = scope or {}
366 366 self.scope_repo_id = scope.get('repo_id')
367 367 self.scope_repo_group_id = scope.get('repo_group_id')
368 368 self.scope_user_group_id = scope.get('user_group_id')
369 369
370 370 self.default_user_id = User.get_default_user(cache=True).user_id
371 371
372 372 self.permissions_repositories = PermOriginDict()
373 373 self.permissions_repository_groups = PermOriginDict()
374 374 self.permissions_user_groups = PermOriginDict()
375 375 self.permissions_global = set()
376 376
377 377 self.default_repo_perms = Permission.get_default_repo_perms(
378 378 self.default_user_id, self.scope_repo_id)
379 379 self.default_repo_groups_perms = Permission.get_default_group_perms(
380 380 self.default_user_id, self.scope_repo_group_id)
381 381 self.default_user_group_perms = \
382 382 Permission.get_default_user_group_perms(
383 383 self.default_user_id, self.scope_user_group_id)
384 384
385 385 def calculate(self):
386 386 if self.user_is_admin:
387 387 return self._admin_permissions()
388 388
389 389 self._calculate_global_default_permissions()
390 390 self._calculate_global_permissions()
391 391 self._calculate_default_permissions()
392 392 self._calculate_repository_permissions()
393 393 self._calculate_repository_group_permissions()
394 394 self._calculate_user_group_permissions()
395 395 return self._permission_structure()
396 396
397 397 def _admin_permissions(self):
398 398 """
399 399 admin user have all default rights for repositories
400 400 and groups set to admin
401 401 """
402 402 self.permissions_global.add('hg.admin')
403 403 self.permissions_global.add('hg.create.write_on_repogroup.true')
404 404
405 405 # repositories
406 406 for perm in self.default_repo_perms:
407 407 r_k = perm.UserRepoToPerm.repository.repo_name
408 408 p = 'repository.admin'
409 409 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
410 410
411 411 # repository groups
412 412 for perm in self.default_repo_groups_perms:
413 413 rg_k = perm.UserRepoGroupToPerm.group.group_name
414 414 p = 'group.admin'
415 415 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
416 416
417 417 # user groups
418 418 for perm in self.default_user_group_perms:
419 419 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
420 420 p = 'usergroup.admin'
421 421 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
422 422
423 423 return self._permission_structure()
424 424
425 425 def _calculate_global_default_permissions(self):
426 426 """
427 427 global permissions taken from the default user
428 428 """
429 429 default_global_perms = UserToPerm.query()\
430 430 .filter(UserToPerm.user_id == self.default_user_id)\
431 431 .options(joinedload(UserToPerm.permission))
432 432
433 433 for perm in default_global_perms:
434 434 self.permissions_global.add(perm.permission.permission_name)
435 435
436 436 def _calculate_global_permissions(self):
437 437 """
438 438 Set global system permissions with user permissions or permissions
439 439 taken from the user groups of the current user.
440 440
441 441 The permissions include repo creating, repo group creating, forking
442 442 etc.
443 443 """
444 444
445 445 # now we read the defined permissions and overwrite what we have set
446 446 # before those can be configured from groups or users explicitly.
447 447
448 448 # TODO: johbo: This seems to be out of sync, find out the reason
449 449 # for the comment below and update it.
450 450
451 451 # In case we want to extend this list we should be always in sync with
452 452 # User.DEFAULT_USER_PERMISSIONS definitions
453 453 _configurable = frozenset([
454 454 'hg.fork.none', 'hg.fork.repository',
455 455 'hg.create.none', 'hg.create.repository',
456 456 'hg.usergroup.create.false', 'hg.usergroup.create.true',
457 457 'hg.repogroup.create.false', 'hg.repogroup.create.true',
458 458 'hg.create.write_on_repogroup.false',
459 459 'hg.create.write_on_repogroup.true',
460 460 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
461 461 ])
462 462
463 463 # USER GROUPS comes first user group global permissions
464 464 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
465 465 .options(joinedload(UserGroupToPerm.permission))\
466 466 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
467 467 UserGroupMember.users_group_id))\
468 468 .filter(UserGroupMember.user_id == self.user_id)\
469 469 .order_by(UserGroupToPerm.users_group_id)\
470 470 .all()
471 471
472 472 # need to group here by groups since user can be in more than
473 473 # one group, so we get all groups
474 474 _explicit_grouped_perms = [
475 475 [x, list(y)] for x, y in
476 476 itertools.groupby(user_perms_from_users_groups,
477 477 lambda _x: _x.users_group)]
478 478
479 479 for gr, perms in _explicit_grouped_perms:
480 480 # since user can be in multiple groups iterate over them and
481 481 # select the lowest permissions first (more explicit)
482 482 # TODO: marcink: do this^^
483 483
484 484 # group doesn't inherit default permissions so we actually set them
485 485 if not gr.inherit_default_permissions:
486 486 # NEED TO IGNORE all previously set configurable permissions
487 487 # and replace them with explicitly set from this user
488 488 # group permissions
489 489 self.permissions_global = self.permissions_global.difference(
490 490 _configurable)
491 491 for perm in perms:
492 492 self.permissions_global.add(perm.permission.permission_name)
493 493
494 494 # user explicit global permissions
495 495 user_perms = Session().query(UserToPerm)\
496 496 .options(joinedload(UserToPerm.permission))\
497 497 .filter(UserToPerm.user_id == self.user_id).all()
498 498
499 499 if not self.inherit_default_permissions:
500 500 # NEED TO IGNORE all configurable permissions and
501 501 # replace them with explicitly set from this user permissions
502 502 self.permissions_global = self.permissions_global.difference(
503 503 _configurable)
504 504 for perm in user_perms:
505 505 self.permissions_global.add(perm.permission.permission_name)
506 506
507 507 def _calculate_default_permissions(self):
508 508 """
509 509 Set default user permissions for repositories, repository groups
510 510 taken from the default user.
511 511
512 512 Calculate inheritance of object permissions based on what we have now
513 513 in GLOBAL permissions. We check if .false is in GLOBAL since this is
514 514 explicitly set. Inherit is the opposite of .false being there.
515 515
516 516 .. note::
517 517
518 518 the syntax is little bit odd but what we need to check here is
519 519 the opposite of .false permission being in the list so even for
520 520 inconsistent state when both .true/.false is there
521 521 .false is more important
522 522
523 523 """
524 524 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
525 525 in self.permissions_global)
526 526
527 527 # defaults for repositories, taken from `default` user permissions
528 528 # on given repo
529 529 for perm in self.default_repo_perms:
530 530 r_k = perm.UserRepoToPerm.repository.repo_name
531 531 o = PermOrigin.REPO_DEFAULT
532 532 if perm.Repository.private and not (
533 533 perm.Repository.user_id == self.user_id):
534 534 # disable defaults for private repos,
535 535 p = 'repository.none'
536 536 o = PermOrigin.REPO_PRIVATE
537 537 elif perm.Repository.user_id == self.user_id:
538 538 # set admin if owner
539 539 p = 'repository.admin'
540 540 o = PermOrigin.REPO_OWNER
541 541 else:
542 542 p = perm.Permission.permission_name
543 543 # if we decide this user isn't inheriting permissions from
544 544 # default user we set him to .none so only explicit
545 545 # permissions work
546 546 if not user_inherit_object_permissions:
547 547 p = 'repository.none'
548 548 self.permissions_repositories[r_k] = p, o
549 549
550 550 # defaults for repository groups taken from `default` user permission
551 551 # on given group
552 552 for perm in self.default_repo_groups_perms:
553 553 rg_k = perm.UserRepoGroupToPerm.group.group_name
554 554 o = PermOrigin.REPOGROUP_DEFAULT
555 555 if perm.RepoGroup.user_id == self.user_id:
556 556 # set admin if owner
557 557 p = 'group.admin'
558 558 o = PermOrigin.REPOGROUP_OWNER
559 559 else:
560 560 p = perm.Permission.permission_name
561 561
562 562 # if we decide this user isn't inheriting permissions from default
563 563 # user we set him to .none so only explicit permissions work
564 564 if not user_inherit_object_permissions:
565 565 p = 'group.none'
566 566 self.permissions_repository_groups[rg_k] = p, o
567 567
568 568 # defaults for user groups taken from `default` user permission
569 569 # on given user group
570 570 for perm in self.default_user_group_perms:
571 571 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
572 572 o = PermOrigin.USERGROUP_DEFAULT
573 573 if perm.UserGroup.user_id == self.user_id:
574 574 # set admin if owner
575 575 p = 'usergroup.admin'
576 576 o = PermOrigin.USERGROUP_OWNER
577 577 else:
578 578 p = perm.Permission.permission_name
579 579
580 580 # if we decide this user isn't inheriting permissions from default
581 581 # user we set him to .none so only explicit permissions work
582 582 if not user_inherit_object_permissions:
583 583 p = 'usergroup.none'
584 584 self.permissions_user_groups[u_k] = p, o
585 585
586 586 def _calculate_repository_permissions(self):
587 587 """
588 588 Repository permissions for the current user.
589 589
590 590 Check if the user is part of user groups for this repository and
591 591 fill in the permission from it. `_choose_permission` decides of which
592 592 permission should be selected based on selected method.
593 593 """
594 594
595 595 # user group for repositories permissions
596 596 user_repo_perms_from_user_group = Permission\
597 597 .get_default_repo_perms_from_user_group(
598 598 self.user_id, self.scope_repo_id)
599 599
600 600 multiple_counter = collections.defaultdict(int)
601 601 for perm in user_repo_perms_from_user_group:
602 602 r_k = perm.UserGroupRepoToPerm.repository.repo_name
603 603 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
604 604 multiple_counter[r_k] += 1
605 605 p = perm.Permission.permission_name
606 606 o = PermOrigin.REPO_USERGROUP % ug_k
607 607
608 608 if perm.Repository.user_id == self.user_id:
609 609 # set admin if owner
610 610 p = 'repository.admin'
611 611 o = PermOrigin.REPO_OWNER
612 612 else:
613 613 if multiple_counter[r_k] > 1:
614 614 cur_perm = self.permissions_repositories[r_k]
615 615 p = self._choose_permission(p, cur_perm)
616 616 self.permissions_repositories[r_k] = p, o
617 617
618 618 # user explicit permissions for repositories, overrides any specified
619 619 # by the group permission
620 620 user_repo_perms = Permission.get_default_repo_perms(
621 621 self.user_id, self.scope_repo_id)
622 622 for perm in user_repo_perms:
623 623 r_k = perm.UserRepoToPerm.repository.repo_name
624 624 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
625 625 # set admin if owner
626 626 if perm.Repository.user_id == self.user_id:
627 627 p = 'repository.admin'
628 628 o = PermOrigin.REPO_OWNER
629 629 else:
630 630 p = perm.Permission.permission_name
631 631 if not self.explicit:
632 632 cur_perm = self.permissions_repositories.get(
633 633 r_k, 'repository.none')
634 634 p = self._choose_permission(p, cur_perm)
635 635 self.permissions_repositories[r_k] = p, o
636 636
637 637 def _calculate_repository_group_permissions(self):
638 638 """
639 639 Repository group permissions for the current user.
640 640
641 641 Check if the user is part of user groups for repository groups and
642 642 fill in the permissions from it. `_choose_permmission` decides of which
643 643 permission should be selected based on selected method.
644 644 """
645 645 # user group for repo groups permissions
646 646 user_repo_group_perms_from_user_group = Permission\
647 647 .get_default_group_perms_from_user_group(
648 648 self.user_id, self.scope_repo_group_id)
649 649
650 650 multiple_counter = collections.defaultdict(int)
651 651 for perm in user_repo_group_perms_from_user_group:
652 652 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
653 653 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
654 654 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
655 655 multiple_counter[g_k] += 1
656 656 p = perm.Permission.permission_name
657 657 if perm.RepoGroup.user_id == self.user_id:
658 658 # set admin if owner, even for member of other user group
659 659 p = 'group.admin'
660 660 o = PermOrigin.REPOGROUP_OWNER
661 661 else:
662 662 if multiple_counter[g_k] > 1:
663 663 cur_perm = self.permissions_repository_groups[g_k]
664 664 p = self._choose_permission(p, cur_perm)
665 665 self.permissions_repository_groups[g_k] = p, o
666 666
667 667 # user explicit permissions for repository groups
668 668 user_repo_groups_perms = Permission.get_default_group_perms(
669 669 self.user_id, self.scope_repo_group_id)
670 670 for perm in user_repo_groups_perms:
671 671 rg_k = perm.UserRepoGroupToPerm.group.group_name
672 672 u_k = perm.UserRepoGroupToPerm.user.username
673 673 o = PermOrigin.REPOGROUP_USER % u_k
674 674
675 675 if perm.RepoGroup.user_id == self.user_id:
676 676 # set admin if owner
677 677 p = 'group.admin'
678 678 o = PermOrigin.REPOGROUP_OWNER
679 679 else:
680 680 p = perm.Permission.permission_name
681 681 if not self.explicit:
682 682 cur_perm = self.permissions_repository_groups.get(
683 683 rg_k, 'group.none')
684 684 p = self._choose_permission(p, cur_perm)
685 685 self.permissions_repository_groups[rg_k] = p, o
686 686
687 687 def _calculate_user_group_permissions(self):
688 688 """
689 689 User group permissions for the current user.
690 690 """
691 691 # user group for user group permissions
692 692 user_group_from_user_group = Permission\
693 693 .get_default_user_group_perms_from_user_group(
694 694 self.user_id, self.scope_user_group_id)
695 695
696 696 multiple_counter = collections.defaultdict(int)
697 697 for perm in user_group_from_user_group:
698 698 g_k = perm.UserGroupUserGroupToPerm\
699 699 .target_user_group.users_group_name
700 700 u_k = perm.UserGroupUserGroupToPerm\
701 701 .user_group.users_group_name
702 702 o = PermOrigin.USERGROUP_USERGROUP % u_k
703 703 multiple_counter[g_k] += 1
704 704 p = perm.Permission.permission_name
705 705
706 706 if perm.UserGroup.user_id == self.user_id:
707 707 # set admin if owner, even for member of other user group
708 708 p = 'usergroup.admin'
709 709 o = PermOrigin.USERGROUP_OWNER
710 710 else:
711 711 if multiple_counter[g_k] > 1:
712 712 cur_perm = self.permissions_user_groups[g_k]
713 713 p = self._choose_permission(p, cur_perm)
714 714 self.permissions_user_groups[g_k] = p, o
715 715
716 716 # user explicit permission for user groups
717 717 user_user_groups_perms = Permission.get_default_user_group_perms(
718 718 self.user_id, self.scope_user_group_id)
719 719 for perm in user_user_groups_perms:
720 720 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
721 721 u_k = perm.UserUserGroupToPerm.user.username
722 722 o = PermOrigin.USERGROUP_USER % u_k
723 723
724 724 if perm.UserGroup.user_id == self.user_id:
725 725 # set admin if owner
726 726 p = 'usergroup.admin'
727 727 o = PermOrigin.USERGROUP_OWNER
728 728 else:
729 729 p = perm.Permission.permission_name
730 730 if not self.explicit:
731 731 cur_perm = self.permissions_user_groups.get(
732 732 ug_k, 'usergroup.none')
733 733 p = self._choose_permission(p, cur_perm)
734 734 self.permissions_user_groups[ug_k] = p, o
735 735
736 736 def _choose_permission(self, new_perm, cur_perm):
737 737 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
738 738 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
739 739 if self.algo == 'higherwin':
740 740 if new_perm_val > cur_perm_val:
741 741 return new_perm
742 742 return cur_perm
743 743 elif self.algo == 'lowerwin':
744 744 if new_perm_val < cur_perm_val:
745 745 return new_perm
746 746 return cur_perm
747 747
748 748 def _permission_structure(self):
749 749 return {
750 750 'global': self.permissions_global,
751 751 'repositories': self.permissions_repositories,
752 752 'repositories_groups': self.permissions_repository_groups,
753 753 'user_groups': self.permissions_user_groups,
754 754 }
755 755
756 756
757 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
757 def allowed_auth_token_access(view_name, whitelist=None, auth_token=None):
758 758 """
759 759 Check if given controller_name is in whitelist of auth token access
760 760 """
761 761 if not whitelist:
762 762 from rhodecode import CONFIG
763 763 whitelist = aslist(
764 764 CONFIG.get('api_access_controllers_whitelist'), sep=',')
765 765 log.debug(
766 766 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
767 767
768 768 auth_token_access_valid = False
769 769 for entry in whitelist:
770 if fnmatch.fnmatch(controller_name, entry):
770 if fnmatch.fnmatch(view_name, entry):
771 771 auth_token_access_valid = True
772 772 break
773 773
774 774 if auth_token_access_valid:
775 log.debug('controller:%s matches entry in whitelist'
776 % (controller_name,))
775 log.debug('view: `%s` matches entry in whitelist: %s'
776 % (view_name, whitelist))
777 777 else:
778 msg = ('controller: %s does *NOT* match any entry in whitelist'
779 % (controller_name,))
778 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
779 % (view_name, whitelist))
780 780 if auth_token:
781 781 # if we use auth token key and don't have access it's a warning
782 782 log.warning(msg)
783 783 else:
784 784 log.debug(msg)
785 785
786 786 return auth_token_access_valid
787 787
788 788
789 789 class AuthUser(object):
790 790 """
791 791 A simple object that handles all attributes of user in RhodeCode
792 792
793 793 It does lookup based on API key,given user, or user present in session
794 794 Then it fills all required information for such user. It also checks if
795 795 anonymous access is enabled and if so, it returns default user as logged in
796 796 """
797 797 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
798 798
799 799 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
800 800
801 801 self.user_id = user_id
802 802 self._api_key = api_key
803 803
804 804 self.api_key = None
805 805 self.feed_token = ''
806 806 self.username = username
807 807 self.ip_addr = ip_addr
808 808 self.name = ''
809 809 self.lastname = ''
810 810 self.first_name = ''
811 811 self.last_name = ''
812 812 self.email = ''
813 813 self.is_authenticated = False
814 814 self.admin = False
815 815 self.inherit_default_permissions = False
816 816 self.password = ''
817 817
818 818 self.anonymous_user = None # propagated on propagate_data
819 819 self.propagate_data()
820 820 self._instance = None
821 821 self._permissions_scoped_cache = {} # used to bind scoped calculation
822 822
823 823 @LazyProperty
824 824 def permissions(self):
825 825 return self.get_perms(user=self, cache=False)
826 826
827 827 def permissions_with_scope(self, scope):
828 828 """
829 829 Call the get_perms function with scoped data. The scope in that function
830 830 narrows the SQL calls to the given ID of objects resulting in fetching
831 831 Just particular permission we want to obtain. If scope is an empty dict
832 832 then it basically narrows the scope to GLOBAL permissions only.
833 833
834 834 :param scope: dict
835 835 """
836 836 if 'repo_name' in scope:
837 837 obj = Repository.get_by_repo_name(scope['repo_name'])
838 838 if obj:
839 839 scope['repo_id'] = obj.repo_id
840 840 _scope = {
841 841 'repo_id': -1,
842 842 'user_group_id': -1,
843 843 'repo_group_id': -1,
844 844 }
845 845 _scope.update(scope)
846 846 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
847 847 _scope.items())))
848 848 if cache_key not in self._permissions_scoped_cache:
849 849 # store in cache to mimic how the @LazyProperty works,
850 850 # the difference here is that we use the unique key calculated
851 851 # from params and values
852 852 res = self.get_perms(user=self, cache=False, scope=_scope)
853 853 self._permissions_scoped_cache[cache_key] = res
854 854 return self._permissions_scoped_cache[cache_key]
855 855
856 856 def get_instance(self):
857 857 return User.get(self.user_id)
858 858
859 859 def update_lastactivity(self):
860 860 if self.user_id:
861 861 User.get(self.user_id).update_lastactivity()
862 862
863 863 def propagate_data(self):
864 864 """
865 865 Fills in user data and propagates values to this instance. Maps fetched
866 866 user attributes to this class instance attributes
867 867 """
868 868 log.debug('starting data propagation for new potential AuthUser')
869 869 user_model = UserModel()
870 870 anon_user = self.anonymous_user = User.get_default_user(cache=True)
871 871 is_user_loaded = False
872 872
873 873 # lookup by userid
874 874 if self.user_id is not None and self.user_id != anon_user.user_id:
875 875 log.debug('Trying Auth User lookup by USER ID: `%s`' % self.user_id)
876 876 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
877 877
878 878 # try go get user by api key
879 879 elif self._api_key and self._api_key != anon_user.api_key:
880 880 log.debug('Trying Auth User lookup by API KEY: `%s`' % self._api_key)
881 881 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
882 882
883 883 # lookup by username
884 884 elif self.username:
885 885 log.debug('Trying Auth User lookup by USER NAME: `%s`' % self.username)
886 886 is_user_loaded = user_model.fill_data(self, username=self.username)
887 887 else:
888 888 log.debug('No data in %s that could been used to log in' % self)
889 889
890 890 if not is_user_loaded:
891 891 log.debug('Failed to load user. Fallback to default user')
892 892 # if we cannot authenticate user try anonymous
893 893 if anon_user.active:
894 894 user_model.fill_data(self, user_id=anon_user.user_id)
895 895 # then we set this user is logged in
896 896 self.is_authenticated = True
897 897 else:
898 898 # in case of disabled anonymous user we reset some of the
899 899 # parameters so such user is "corrupted", skipping the fill_data
900 900 for attr in ['user_id', 'username', 'admin', 'active']:
901 901 setattr(self, attr, None)
902 902 self.is_authenticated = False
903 903
904 904 if not self.username:
905 905 self.username = 'None'
906 906
907 907 log.debug('Auth User is now %s' % self)
908 908
909 909 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
910 910 cache=False):
911 911 """
912 912 Fills user permission attribute with permissions taken from database
913 913 works for permissions given for repositories, and for permissions that
914 914 are granted to groups
915 915
916 916 :param user: instance of User object from database
917 917 :param explicit: In case there are permissions both for user and a group
918 918 that user is part of, explicit flag will defiine if user will
919 919 explicitly override permissions from group, if it's False it will
920 920 make decision based on the algo
921 921 :param algo: algorithm to decide what permission should be choose if
922 922 it's multiple defined, eg user in two different groups. It also
923 923 decides if explicit flag is turned off how to specify the permission
924 924 for case when user is in a group + have defined separate permission
925 925 """
926 926 user_id = user.user_id
927 927 user_is_admin = user.is_admin
928 928
929 929 # inheritance of global permissions like create repo/fork repo etc
930 930 user_inherit_default_permissions = user.inherit_default_permissions
931 931
932 932 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
933 933 compute = caches.conditional_cache(
934 934 'short_term', 'cache_desc',
935 935 condition=cache, func=_cached_perms_data)
936 936 result = compute(user_id, scope, user_is_admin,
937 937 user_inherit_default_permissions, explicit, algo)
938 938
939 939 result_repr = []
940 940 for k in result:
941 941 result_repr.append((k, len(result[k])))
942 942
943 943 log.debug('PERMISSION tree computed %s' % (result_repr,))
944 944 return result
945 945
946 946 @property
947 947 def is_default(self):
948 948 return self.username == User.DEFAULT_USER
949 949
950 950 @property
951 951 def is_admin(self):
952 952 return self.admin
953 953
954 954 @property
955 955 def is_user_object(self):
956 956 return self.user_id is not None
957 957
958 958 @property
959 959 def repositories_admin(self):
960 960 """
961 961 Returns list of repositories you're an admin of
962 962 """
963 963 return [
964 964 x[0] for x in self.permissions['repositories'].iteritems()
965 965 if x[1] == 'repository.admin']
966 966
967 967 @property
968 968 def repository_groups_admin(self):
969 969 """
970 970 Returns list of repository groups you're an admin of
971 971 """
972 972 return [
973 973 x[0] for x in self.permissions['repositories_groups'].iteritems()
974 974 if x[1] == 'group.admin']
975 975
976 976 @property
977 977 def user_groups_admin(self):
978 978 """
979 979 Returns list of user groups you're an admin of
980 980 """
981 981 return [
982 982 x[0] for x in self.permissions['user_groups'].iteritems()
983 983 if x[1] == 'usergroup.admin']
984 984
985 985 @property
986 986 def ip_allowed(self):
987 987 """
988 988 Checks if ip_addr used in constructor is allowed from defined list of
989 989 allowed ip_addresses for user
990 990
991 991 :returns: boolean, True if ip is in allowed ip range
992 992 """
993 993 # check IP
994 994 inherit = self.inherit_default_permissions
995 995 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
996 996 inherit_from_default=inherit)
997 997 @property
998 998 def personal_repo_group(self):
999 999 return RepoGroup.get_user_personal_repo_group(self.user_id)
1000 1000
1001 1001 @classmethod
1002 1002 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1003 1003 allowed_ips = AuthUser.get_allowed_ips(
1004 1004 user_id, cache=True, inherit_from_default=inherit_from_default)
1005 1005 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1006 1006 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
1007 1007 return True
1008 1008 else:
1009 1009 log.info('Access for IP:%s forbidden, '
1010 1010 'not in %s' % (ip_addr, allowed_ips))
1011 1011 return False
1012 1012
1013 1013 def __repr__(self):
1014 1014 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1015 1015 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1016 1016
1017 1017 def set_authenticated(self, authenticated=True):
1018 1018 if self.user_id != self.anonymous_user.user_id:
1019 1019 self.is_authenticated = authenticated
1020 1020
1021 1021 def get_cookie_store(self):
1022 1022 return {
1023 1023 'username': self.username,
1024 1024 'password': md5(self.password),
1025 1025 'user_id': self.user_id,
1026 1026 'is_authenticated': self.is_authenticated
1027 1027 }
1028 1028
1029 1029 @classmethod
1030 1030 def from_cookie_store(cls, cookie_store):
1031 1031 """
1032 1032 Creates AuthUser from a cookie store
1033 1033
1034 1034 :param cls:
1035 1035 :param cookie_store:
1036 1036 """
1037 1037 user_id = cookie_store.get('user_id')
1038 1038 username = cookie_store.get('username')
1039 1039 api_key = cookie_store.get('api_key')
1040 1040 return AuthUser(user_id, api_key, username)
1041 1041
1042 1042 @classmethod
1043 1043 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1044 1044 _set = set()
1045 1045
1046 1046 if inherit_from_default:
1047 1047 default_ips = UserIpMap.query().filter(
1048 1048 UserIpMap.user == User.get_default_user(cache=True))
1049 1049 if cache:
1050 1050 default_ips = default_ips.options(
1051 1051 FromCache("sql_cache_short", "get_user_ips_default"))
1052 1052
1053 1053 # populate from default user
1054 1054 for ip in default_ips:
1055 1055 try:
1056 1056 _set.add(ip.ip_addr)
1057 1057 except ObjectDeletedError:
1058 1058 # since we use heavy caching sometimes it happens that
1059 1059 # we get deleted objects here, we just skip them
1060 1060 pass
1061 1061
1062 1062 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1063 1063 if cache:
1064 1064 user_ips = user_ips.options(
1065 1065 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1066 1066
1067 1067 for ip in user_ips:
1068 1068 try:
1069 1069 _set.add(ip.ip_addr)
1070 1070 except ObjectDeletedError:
1071 1071 # since we use heavy caching sometimes it happens that we get
1072 1072 # deleted objects here, we just skip them
1073 1073 pass
1074 1074 return _set or set(['0.0.0.0/0', '::/0'])
1075 1075
1076 1076
1077 1077 def set_available_permissions(config):
1078 1078 """
1079 1079 This function will propagate pylons globals with all available defined
1080 1080 permission given in db. We don't want to check each time from db for new
1081 1081 permissions since adding a new permission also requires application restart
1082 1082 ie. to decorate new views with the newly created permission
1083 1083
1084 1084 :param config: current pylons config instance
1085 1085
1086 1086 """
1087 1087 log.info('getting information about all available permissions')
1088 1088 try:
1089 1089 sa = meta.Session
1090 1090 all_perms = sa.query(Permission).all()
1091 1091 config['available_permissions'] = [x.permission_name for x in all_perms]
1092 1092 except Exception:
1093 1093 log.error(traceback.format_exc())
1094 1094 finally:
1095 1095 meta.Session.remove()
1096 1096
1097 1097
1098 1098 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1099 1099 """
1100 1100 Return the current authentication token, creating one if one doesn't
1101 1101 already exist and the save_if_missing flag is present.
1102 1102
1103 1103 :param session: pass in the pylons session, else we use the global ones
1104 1104 :param force_new: force to re-generate the token and store it in session
1105 1105 :param save_if_missing: save the newly generated token if it's missing in
1106 1106 session
1107 1107 """
1108 1108 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1109 1109 # from pyramid.csrf import get_csrf_token
1110 1110
1111 1111 if not session:
1112 1112 from pylons import session
1113 1113
1114 1114 if (csrf_token_key not in session and save_if_missing) or force_new:
1115 1115 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1116 1116 session[csrf_token_key] = token
1117 1117 if hasattr(session, 'save'):
1118 1118 session.save()
1119 1119 return session.get(csrf_token_key)
1120 1120
1121 1121
1122 1122 def get_request(perm_class):
1123 1123 from pyramid.threadlocal import get_current_request
1124 1124 pyramid_request = get_current_request()
1125 1125 if not pyramid_request:
1126 1126 # return global request of pylons in case pyramid isn't available
1127 1127 # NOTE(marcink): this should be removed after migration to pyramid
1128 1128 from pylons import request
1129 1129 return request
1130 1130 return pyramid_request
1131 1131
1132 1132
1133 1133 # CHECK DECORATORS
1134 1134 class CSRFRequired(object):
1135 1135 """
1136 1136 Decorator for authenticating a form
1137 1137
1138 1138 This decorator uses an authorization token stored in the client's
1139 1139 session for prevention of certain Cross-site request forgery (CSRF)
1140 1140 attacks (See
1141 1141 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1142 1142 information).
1143 1143
1144 1144 For use with the ``webhelpers.secure_form`` helper functions.
1145 1145
1146 1146 """
1147 1147 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1148 1148 except_methods=None):
1149 1149 self.token = token
1150 1150 self.header = header
1151 1151 self.except_methods = except_methods or []
1152 1152
1153 1153 def __call__(self, func):
1154 1154 return get_cython_compat_decorator(self.__wrapper, func)
1155 1155
1156 1156 def _get_csrf(self, _request):
1157 1157 return _request.POST.get(self.token, _request.headers.get(self.header))
1158 1158
1159 1159 def check_csrf(self, _request, cur_token):
1160 1160 supplied_token = self._get_csrf(_request)
1161 1161 return supplied_token and supplied_token == cur_token
1162 1162
1163 1163 def _get_request(self):
1164 1164 return get_request(self)
1165 1165
1166 1166 def __wrapper(self, func, *fargs, **fkwargs):
1167 1167 request = self._get_request()
1168 1168
1169 1169 if request.method in self.except_methods:
1170 1170 return func(*fargs, **fkwargs)
1171 1171
1172 1172 cur_token = get_csrf_token(save_if_missing=False)
1173 1173 if self.check_csrf(request, cur_token):
1174 1174 if request.POST.get(self.token):
1175 1175 del request.POST[self.token]
1176 1176 return func(*fargs, **fkwargs)
1177 1177 else:
1178 1178 reason = 'token-missing'
1179 1179 supplied_token = self._get_csrf(request)
1180 1180 if supplied_token and cur_token != supplied_token:
1181 1181 reason = 'token-mismatch [%s:%s]' % (
1182 1182 cur_token or ''[:6], supplied_token or ''[:6])
1183 1183
1184 1184 csrf_message = \
1185 1185 ("Cross-site request forgery detected, request denied. See "
1186 1186 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1187 1187 "more information.")
1188 1188 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1189 1189 'REMOTE_ADDR:%s, HEADERS:%s' % (
1190 1190 request, reason, request.remote_addr, request.headers))
1191 1191
1192 1192 raise HTTPForbidden(explanation=csrf_message)
1193 1193
1194 1194
1195 1195 class LoginRequired(object):
1196 1196 """
1197 1197 Must be logged in to execute this function else
1198 1198 redirect to login page
1199 1199
1200 1200 :param api_access: if enabled this checks only for valid auth token
1201 1201 and grants access based on valid token
1202 1202 """
1203 1203 def __init__(self, auth_token_access=None):
1204 1204 self.auth_token_access = auth_token_access
1205 1205
1206 1206 def __call__(self, func):
1207 1207 return get_cython_compat_decorator(self.__wrapper, func)
1208 1208
1209 1209 def _get_request(self):
1210 1210 return get_request(self)
1211 1211
1212 1212 def __wrapper(self, func, *fargs, **fkwargs):
1213 1213 from rhodecode.lib import helpers as h
1214 1214 cls = fargs[0]
1215 1215 user = cls._rhodecode_user
1216 1216 request = self._get_request()
1217 1217
1218 1218 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1219 1219 log.debug('Starting login restriction checks for user: %s' % (user,))
1220 1220 # check if our IP is allowed
1221 1221 ip_access_valid = True
1222 1222 if not user.ip_allowed:
1223 1223 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1224 1224 category='warning')
1225 1225 ip_access_valid = False
1226 1226
1227 1227 # check if we used an APIKEY and it's a valid one
1228 1228 # defined white-list of controllers which API access will be enabled
1229 1229 _auth_token = request.GET.get(
1230 1230 'auth_token', '') or request.GET.get('api_key', '')
1231 1231 auth_token_access_valid = allowed_auth_token_access(
1232 1232 loc, auth_token=_auth_token)
1233 1233
1234 1234 # explicit controller is enabled or API is in our whitelist
1235 1235 if self.auth_token_access or auth_token_access_valid:
1236 1236 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1237 1237 db_user = user.get_instance()
1238 1238
1239 1239 if db_user:
1240 1240 if self.auth_token_access:
1241 1241 roles = self.auth_token_access
1242 1242 else:
1243 1243 roles = [UserApiKeys.ROLE_HTTP]
1244 1244 token_match = db_user.authenticate_by_token(
1245 1245 _auth_token, roles=roles)
1246 1246 else:
1247 1247 log.debug('Unable to fetch db instance for auth user: %s', user)
1248 1248 token_match = False
1249 1249
1250 1250 if _auth_token and token_match:
1251 1251 auth_token_access_valid = True
1252 1252 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1253 1253 else:
1254 1254 auth_token_access_valid = False
1255 1255 if not _auth_token:
1256 1256 log.debug("AUTH TOKEN *NOT* present in request")
1257 1257 else:
1258 1258 log.warning(
1259 1259 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1260 1260
1261 1261 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1262 1262 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1263 1263 else 'AUTH_TOKEN_AUTH'
1264 1264
1265 1265 if ip_access_valid and (
1266 1266 user.is_authenticated or auth_token_access_valid):
1267 1267 log.info(
1268 1268 'user %s authenticating with:%s IS authenticated on func %s'
1269 1269 % (user, reason, loc))
1270 1270
1271 1271 # update user data to check last activity
1272 1272 user.update_lastactivity()
1273 1273 Session().commit()
1274 1274 return func(*fargs, **fkwargs)
1275 1275 else:
1276 1276 log.warning(
1277 1277 'user %s authenticating with:%s NOT authenticated on '
1278 1278 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1279 1279 % (user, reason, loc, ip_access_valid,
1280 1280 auth_token_access_valid))
1281 1281 # we preserve the get PARAM
1282 1282 came_from = request.path_qs
1283 1283 log.debug('redirecting to login page with %s' % (came_from,))
1284 1284 raise HTTPFound(
1285 1285 h.route_path('login', _query={'came_from': came_from}))
1286 1286
1287 1287
1288 1288 class NotAnonymous(object):
1289 1289 """
1290 1290 Must be logged in to execute this function else
1291 1291 redirect to login page
1292 1292 """
1293 1293
1294 1294 def __call__(self, func):
1295 1295 return get_cython_compat_decorator(self.__wrapper, func)
1296 1296
1297 1297 def _get_request(self):
1298 1298 return get_request(self)
1299 1299
1300 1300 def __wrapper(self, func, *fargs, **fkwargs):
1301 1301 import rhodecode.lib.helpers as h
1302 1302 cls = fargs[0]
1303 1303 self.user = cls._rhodecode_user
1304 1304 request = self._get_request()
1305 1305
1306 1306 log.debug('Checking if user is not anonymous @%s' % cls)
1307 1307
1308 1308 anonymous = self.user.username == User.DEFAULT_USER
1309 1309
1310 1310 if anonymous:
1311 1311 came_from = request.path_qs
1312 1312 h.flash(_('You need to be a registered user to '
1313 1313 'perform this action'),
1314 1314 category='warning')
1315 1315 raise HTTPFound(
1316 1316 h.route_path('login', _query={'came_from': came_from}))
1317 1317 else:
1318 1318 return func(*fargs, **fkwargs)
1319 1319
1320 1320
1321 1321 class XHRRequired(object):
1322 1322 # TODO(marcink): remove this in favor of the predicates in pyramid routes
1323 1323
1324 1324 def __call__(self, func):
1325 1325 return get_cython_compat_decorator(self.__wrapper, func)
1326 1326
1327 1327 def _get_request(self):
1328 1328 return get_request(self)
1329 1329
1330 1330 def __wrapper(self, func, *fargs, **fkwargs):
1331 1331 from pylons.controllers.util import abort
1332 1332 request = self._get_request()
1333 1333
1334 1334 log.debug('Checking if request is XMLHttpRequest (XHR)')
1335 1335 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1336 1336
1337 1337 if not request.is_xhr:
1338 1338 abort(400, detail=xhr_message)
1339 1339
1340 1340 return func(*fargs, **fkwargs)
1341 1341
1342 1342
1343 1343 class HasAcceptedRepoType(object):
1344 1344 """
1345 1345 Check if requested repo is within given repo type aliases
1346 1346 """
1347 1347
1348 1348 # TODO(marcink): remove this in favor of the predicates in pyramid routes
1349 1349
1350 1350 def __init__(self, *repo_type_list):
1351 1351 self.repo_type_list = set(repo_type_list)
1352 1352
1353 1353 def __call__(self, func):
1354 1354 return get_cython_compat_decorator(self.__wrapper, func)
1355 1355
1356 1356 def __wrapper(self, func, *fargs, **fkwargs):
1357 1357 import rhodecode.lib.helpers as h
1358 1358 cls = fargs[0]
1359 1359 rhodecode_repo = cls.rhodecode_repo
1360 1360
1361 1361 log.debug('%s checking repo type for %s in %s',
1362 1362 self.__class__.__name__,
1363 1363 rhodecode_repo.alias, self.repo_type_list)
1364 1364
1365 1365 if rhodecode_repo.alias in self.repo_type_list:
1366 1366 return func(*fargs, **fkwargs)
1367 1367 else:
1368 1368 h.flash(h.literal(
1369 1369 _('Action not supported for %s.' % rhodecode_repo.alias)),
1370 1370 category='warning')
1371 1371 raise HTTPFound(
1372 1372 h.route_path('repo_summary',
1373 1373 repo_name=cls.rhodecode_db_repo.repo_name))
1374 1374
1375 1375
1376 1376 class PermsDecorator(object):
1377 1377 """
1378 1378 Base class for controller decorators, we extract the current user from
1379 1379 the class itself, which has it stored in base controllers
1380 1380 """
1381 1381
1382 1382 def __init__(self, *required_perms):
1383 1383 self.required_perms = set(required_perms)
1384 1384
1385 1385 def __call__(self, func):
1386 1386 return get_cython_compat_decorator(self.__wrapper, func)
1387 1387
1388 1388 def _get_request(self):
1389 1389 return get_request(self)
1390 1390
1391 1391 def _get_came_from(self):
1392 1392 _request = self._get_request()
1393 1393
1394 1394 # both pylons/pyramid has this attribute
1395 1395 return _request.path_qs
1396 1396
1397 1397 def __wrapper(self, func, *fargs, **fkwargs):
1398 1398 import rhodecode.lib.helpers as h
1399 1399 cls = fargs[0]
1400 1400 _user = cls._rhodecode_user
1401 1401
1402 1402 log.debug('checking %s permissions %s for %s %s',
1403 1403 self.__class__.__name__, self.required_perms, cls, _user)
1404 1404
1405 1405 if self.check_permissions(_user):
1406 1406 log.debug('Permission granted for %s %s', cls, _user)
1407 1407 return func(*fargs, **fkwargs)
1408 1408
1409 1409 else:
1410 1410 log.debug('Permission denied for %s %s', cls, _user)
1411 1411 anonymous = _user.username == User.DEFAULT_USER
1412 1412
1413 1413 if anonymous:
1414 1414 came_from = self._get_came_from()
1415 1415 h.flash(_('You need to be signed in to view this page'),
1416 1416 category='warning')
1417 1417 raise HTTPFound(
1418 1418 h.route_path('login', _query={'came_from': came_from}))
1419 1419
1420 1420 else:
1421 1421 # redirect with 404 to prevent resource discovery
1422 1422 raise HTTPNotFound()
1423 1423
1424 1424 def check_permissions(self, user):
1425 1425 """Dummy function for overriding"""
1426 1426 raise NotImplementedError(
1427 1427 'You have to write this function in child class')
1428 1428
1429 1429
1430 1430 class HasPermissionAllDecorator(PermsDecorator):
1431 1431 """
1432 1432 Checks for access permission for all given predicates. All of them
1433 1433 have to be meet in order to fulfill the request
1434 1434 """
1435 1435
1436 1436 def check_permissions(self, user):
1437 1437 perms = user.permissions_with_scope({})
1438 1438 if self.required_perms.issubset(perms['global']):
1439 1439 return True
1440 1440 return False
1441 1441
1442 1442
1443 1443 class HasPermissionAnyDecorator(PermsDecorator):
1444 1444 """
1445 1445 Checks for access permission for any of given predicates. In order to
1446 1446 fulfill the request any of predicates must be meet
1447 1447 """
1448 1448
1449 1449 def check_permissions(self, user):
1450 1450 perms = user.permissions_with_scope({})
1451 1451 if self.required_perms.intersection(perms['global']):
1452 1452 return True
1453 1453 return False
1454 1454
1455 1455
1456 1456 class HasRepoPermissionAllDecorator(PermsDecorator):
1457 1457 """
1458 1458 Checks for access permission for all given predicates for specific
1459 1459 repository. All of them have to be meet in order to fulfill the request
1460 1460 """
1461 1461 def _get_repo_name(self):
1462 1462 _request = self._get_request()
1463 1463 return get_repo_slug(_request)
1464 1464
1465 1465 def check_permissions(self, user):
1466 1466 perms = user.permissions
1467 1467 repo_name = self._get_repo_name()
1468 1468
1469 1469 try:
1470 1470 user_perms = set([perms['repositories'][repo_name]])
1471 1471 except KeyError:
1472 1472 log.debug('cannot locate repo with name: `%s` in permissions defs',
1473 1473 repo_name)
1474 1474 return False
1475 1475
1476 1476 log.debug('checking `%s` permissions for repo `%s`',
1477 1477 user_perms, repo_name)
1478 1478 if self.required_perms.issubset(user_perms):
1479 1479 return True
1480 1480 return False
1481 1481
1482 1482
1483 1483 class HasRepoPermissionAnyDecorator(PermsDecorator):
1484 1484 """
1485 1485 Checks for access permission for any of given predicates for specific
1486 1486 repository. In order to fulfill the request any of predicates must be meet
1487 1487 """
1488 1488 def _get_repo_name(self):
1489 1489 _request = self._get_request()
1490 1490 return get_repo_slug(_request)
1491 1491
1492 1492 def check_permissions(self, user):
1493 1493 perms = user.permissions
1494 1494 repo_name = self._get_repo_name()
1495 1495
1496 1496 try:
1497 1497 user_perms = set([perms['repositories'][repo_name]])
1498 1498 except KeyError:
1499 1499 log.debug('cannot locate repo with name: `%s` in permissions defs',
1500 1500 repo_name)
1501 1501 return False
1502 1502
1503 1503 log.debug('checking `%s` permissions for repo `%s`',
1504 1504 user_perms, repo_name)
1505 1505 if self.required_perms.intersection(user_perms):
1506 1506 return True
1507 1507 return False
1508 1508
1509 1509
1510 1510 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1511 1511 """
1512 1512 Checks for access permission for all given predicates for specific
1513 1513 repository group. All of them have to be meet in order to
1514 1514 fulfill the request
1515 1515 """
1516 1516 def _get_repo_group_name(self):
1517 1517 _request = self._get_request()
1518 1518 return get_repo_group_slug(_request)
1519 1519
1520 1520 def check_permissions(self, user):
1521 1521 perms = user.permissions
1522 1522 group_name = self._get_repo_group_name()
1523 1523 try:
1524 1524 user_perms = set([perms['repositories_groups'][group_name]])
1525 1525 except KeyError:
1526 1526 log.debug('cannot locate repo group with name: `%s` in permissions defs',
1527 1527 group_name)
1528 1528 return False
1529 1529
1530 1530 log.debug('checking `%s` permissions for repo group `%s`',
1531 1531 user_perms, group_name)
1532 1532 if self.required_perms.issubset(user_perms):
1533 1533 return True
1534 1534 return False
1535 1535
1536 1536
1537 1537 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1538 1538 """
1539 1539 Checks for access permission for any of given predicates for specific
1540 1540 repository group. In order to fulfill the request any
1541 1541 of predicates must be met
1542 1542 """
1543 1543 def _get_repo_group_name(self):
1544 1544 _request = self._get_request()
1545 1545 return get_repo_group_slug(_request)
1546 1546
1547 1547 def check_permissions(self, user):
1548 1548 perms = user.permissions
1549 1549 group_name = self._get_repo_group_name()
1550 1550
1551 1551 try:
1552 1552 user_perms = set([perms['repositories_groups'][group_name]])
1553 1553 except KeyError:
1554 1554 log.debug('cannot locate repo group with name: `%s` in permissions defs',
1555 1555 group_name)
1556 1556 return False
1557 1557
1558 1558 log.debug('checking `%s` permissions for repo group `%s`',
1559 1559 user_perms, group_name)
1560 1560 if self.required_perms.intersection(user_perms):
1561 1561 return True
1562 1562 return False
1563 1563
1564 1564
1565 1565 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1566 1566 """
1567 1567 Checks for access permission for all given predicates for specific
1568 1568 user group. All of them have to be meet in order to fulfill the request
1569 1569 """
1570 1570 def _get_user_group_name(self):
1571 1571 _request = self._get_request()
1572 1572 return get_user_group_slug(_request)
1573 1573
1574 1574 def check_permissions(self, user):
1575 1575 perms = user.permissions
1576 1576 group_name = self._get_user_group_name()
1577 1577 try:
1578 1578 user_perms = set([perms['user_groups'][group_name]])
1579 1579 except KeyError:
1580 1580 return False
1581 1581
1582 1582 if self.required_perms.issubset(user_perms):
1583 1583 return True
1584 1584 return False
1585 1585
1586 1586
1587 1587 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1588 1588 """
1589 1589 Checks for access permission for any of given predicates for specific
1590 1590 user group. In order to fulfill the request any of predicates must be meet
1591 1591 """
1592 1592 def _get_user_group_name(self):
1593 1593 _request = self._get_request()
1594 1594 return get_user_group_slug(_request)
1595 1595
1596 1596 def check_permissions(self, user):
1597 1597 perms = user.permissions
1598 1598 group_name = self._get_user_group_name()
1599 1599 try:
1600 1600 user_perms = set([perms['user_groups'][group_name]])
1601 1601 except KeyError:
1602 1602 return False
1603 1603
1604 1604 if self.required_perms.intersection(user_perms):
1605 1605 return True
1606 1606 return False
1607 1607
1608 1608
1609 1609 # CHECK FUNCTIONS
1610 1610 class PermsFunction(object):
1611 1611 """Base function for other check functions"""
1612 1612
1613 1613 def __init__(self, *perms):
1614 1614 self.required_perms = set(perms)
1615 1615 self.repo_name = None
1616 1616 self.repo_group_name = None
1617 1617 self.user_group_name = None
1618 1618
1619 1619 def __bool__(self):
1620 1620 frame = inspect.currentframe()
1621 1621 stack_trace = traceback.format_stack(frame)
1622 1622 log.error('Checking bool value on a class instance of perm '
1623 1623 'function is not allowed: %s' % ''.join(stack_trace))
1624 1624 # rather than throwing errors, here we always return False so if by
1625 1625 # accident someone checks truth for just an instance it will always end
1626 1626 # up in returning False
1627 1627 return False
1628 1628 __nonzero__ = __bool__
1629 1629
1630 1630 def __call__(self, check_location='', user=None):
1631 1631 if not user:
1632 1632 log.debug('Using user attribute from global request')
1633 1633 # TODO: remove this someday,put as user as attribute here
1634 1634 request = self._get_request()
1635 1635 user = request.user
1636 1636
1637 1637 # init auth user if not already given
1638 1638 if not isinstance(user, AuthUser):
1639 1639 log.debug('Wrapping user %s into AuthUser', user)
1640 1640 user = AuthUser(user.user_id)
1641 1641
1642 1642 cls_name = self.__class__.__name__
1643 1643 check_scope = self._get_check_scope(cls_name)
1644 1644 check_location = check_location or 'unspecified location'
1645 1645
1646 1646 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1647 1647 self.required_perms, user, check_scope, check_location)
1648 1648 if not user:
1649 1649 log.warning('Empty user given for permission check')
1650 1650 return False
1651 1651
1652 1652 if self.check_permissions(user):
1653 1653 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1654 1654 check_scope, user, check_location)
1655 1655 return True
1656 1656
1657 1657 else:
1658 1658 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1659 1659 check_scope, user, check_location)
1660 1660 return False
1661 1661
1662 1662 def _get_request(self):
1663 1663 return get_request(self)
1664 1664
1665 1665 def _get_check_scope(self, cls_name):
1666 1666 return {
1667 1667 'HasPermissionAll': 'GLOBAL',
1668 1668 'HasPermissionAny': 'GLOBAL',
1669 1669 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1670 1670 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1671 1671 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1672 1672 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1673 1673 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1674 1674 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1675 1675 }.get(cls_name, '?:%s' % cls_name)
1676 1676
1677 1677 def check_permissions(self, user):
1678 1678 """Dummy function for overriding"""
1679 1679 raise Exception('You have to write this function in child class')
1680 1680
1681 1681
1682 1682 class HasPermissionAll(PermsFunction):
1683 1683 def check_permissions(self, user):
1684 1684 perms = user.permissions_with_scope({})
1685 1685 if self.required_perms.issubset(perms.get('global')):
1686 1686 return True
1687 1687 return False
1688 1688
1689 1689
1690 1690 class HasPermissionAny(PermsFunction):
1691 1691 def check_permissions(self, user):
1692 1692 perms = user.permissions_with_scope({})
1693 1693 if self.required_perms.intersection(perms.get('global')):
1694 1694 return True
1695 1695 return False
1696 1696
1697 1697
1698 1698 class HasRepoPermissionAll(PermsFunction):
1699 1699 def __call__(self, repo_name=None, check_location='', user=None):
1700 1700 self.repo_name = repo_name
1701 1701 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1702 1702
1703 1703 def _get_repo_name(self):
1704 1704 if not self.repo_name:
1705 1705 _request = self._get_request()
1706 1706 self.repo_name = get_repo_slug(_request)
1707 1707 return self.repo_name
1708 1708
1709 1709 def check_permissions(self, user):
1710 1710 self.repo_name = self._get_repo_name()
1711 1711 perms = user.permissions
1712 1712 try:
1713 1713 user_perms = set([perms['repositories'][self.repo_name]])
1714 1714 except KeyError:
1715 1715 return False
1716 1716 if self.required_perms.issubset(user_perms):
1717 1717 return True
1718 1718 return False
1719 1719
1720 1720
1721 1721 class HasRepoPermissionAny(PermsFunction):
1722 1722 def __call__(self, repo_name=None, check_location='', user=None):
1723 1723 self.repo_name = repo_name
1724 1724 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1725 1725
1726 1726 def _get_repo_name(self):
1727 1727 if not self.repo_name:
1728 1728 _request = self._get_request()
1729 1729 self.repo_name = get_repo_slug(_request)
1730 1730 return self.repo_name
1731 1731
1732 1732 def check_permissions(self, user):
1733 1733 self.repo_name = self._get_repo_name()
1734 1734 perms = user.permissions
1735 1735 try:
1736 1736 user_perms = set([perms['repositories'][self.repo_name]])
1737 1737 except KeyError:
1738 1738 return False
1739 1739 if self.required_perms.intersection(user_perms):
1740 1740 return True
1741 1741 return False
1742 1742
1743 1743
1744 1744 class HasRepoGroupPermissionAny(PermsFunction):
1745 1745 def __call__(self, group_name=None, check_location='', user=None):
1746 1746 self.repo_group_name = group_name
1747 1747 return super(HasRepoGroupPermissionAny, self).__call__(
1748 1748 check_location, user)
1749 1749
1750 1750 def check_permissions(self, user):
1751 1751 perms = user.permissions
1752 1752 try:
1753 1753 user_perms = set(
1754 1754 [perms['repositories_groups'][self.repo_group_name]])
1755 1755 except KeyError:
1756 1756 return False
1757 1757 if self.required_perms.intersection(user_perms):
1758 1758 return True
1759 1759 return False
1760 1760
1761 1761
1762 1762 class HasRepoGroupPermissionAll(PermsFunction):
1763 1763 def __call__(self, group_name=None, check_location='', user=None):
1764 1764 self.repo_group_name = group_name
1765 1765 return super(HasRepoGroupPermissionAll, self).__call__(
1766 1766 check_location, user)
1767 1767
1768 1768 def check_permissions(self, user):
1769 1769 perms = user.permissions
1770 1770 try:
1771 1771 user_perms = set(
1772 1772 [perms['repositories_groups'][self.repo_group_name]])
1773 1773 except KeyError:
1774 1774 return False
1775 1775 if self.required_perms.issubset(user_perms):
1776 1776 return True
1777 1777 return False
1778 1778
1779 1779
1780 1780 class HasUserGroupPermissionAny(PermsFunction):
1781 1781 def __call__(self, user_group_name=None, check_location='', user=None):
1782 1782 self.user_group_name = user_group_name
1783 1783 return super(HasUserGroupPermissionAny, self).__call__(
1784 1784 check_location, user)
1785 1785
1786 1786 def check_permissions(self, user):
1787 1787 perms = user.permissions
1788 1788 try:
1789 1789 user_perms = set([perms['user_groups'][self.user_group_name]])
1790 1790 except KeyError:
1791 1791 return False
1792 1792 if self.required_perms.intersection(user_perms):
1793 1793 return True
1794 1794 return False
1795 1795
1796 1796
1797 1797 class HasUserGroupPermissionAll(PermsFunction):
1798 1798 def __call__(self, user_group_name=None, check_location='', user=None):
1799 1799 self.user_group_name = user_group_name
1800 1800 return super(HasUserGroupPermissionAll, self).__call__(
1801 1801 check_location, user)
1802 1802
1803 1803 def check_permissions(self, user):
1804 1804 perms = user.permissions
1805 1805 try:
1806 1806 user_perms = set([perms['user_groups'][self.user_group_name]])
1807 1807 except KeyError:
1808 1808 return False
1809 1809 if self.required_perms.issubset(user_perms):
1810 1810 return True
1811 1811 return False
1812 1812
1813 1813
1814 1814 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1815 1815 class HasPermissionAnyMiddleware(object):
1816 1816 def __init__(self, *perms):
1817 1817 self.required_perms = set(perms)
1818 1818
1819 1819 def __call__(self, user, repo_name):
1820 1820 # repo_name MUST be unicode, since we handle keys in permission
1821 1821 # dict by unicode
1822 1822 repo_name = safe_unicode(repo_name)
1823 1823 user = AuthUser(user.user_id)
1824 1824 log.debug(
1825 1825 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1826 1826 self.required_perms, user, repo_name)
1827 1827
1828 1828 if self.check_permissions(user, repo_name):
1829 1829 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1830 1830 repo_name, user, 'PermissionMiddleware')
1831 1831 return True
1832 1832
1833 1833 else:
1834 1834 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1835 1835 repo_name, user, 'PermissionMiddleware')
1836 1836 return False
1837 1837
1838 1838 def check_permissions(self, user, repo_name):
1839 1839 perms = user.permissions_with_scope({'repo_name': repo_name})
1840 1840
1841 1841 try:
1842 1842 user_perms = set([perms['repositories'][repo_name]])
1843 1843 except Exception:
1844 1844 log.exception('Error while accessing user permissions')
1845 1845 return False
1846 1846
1847 1847 if self.required_perms.intersection(user_perms):
1848 1848 return True
1849 1849 return False
1850 1850
1851 1851
1852 1852 # SPECIAL VERSION TO HANDLE API AUTH
1853 1853 class _BaseApiPerm(object):
1854 1854 def __init__(self, *perms):
1855 1855 self.required_perms = set(perms)
1856 1856
1857 1857 def __call__(self, check_location=None, user=None, repo_name=None,
1858 1858 group_name=None, user_group_name=None):
1859 1859 cls_name = self.__class__.__name__
1860 1860 check_scope = 'global:%s' % (self.required_perms,)
1861 1861 if repo_name:
1862 1862 check_scope += ', repo_name:%s' % (repo_name,)
1863 1863
1864 1864 if group_name:
1865 1865 check_scope += ', repo_group_name:%s' % (group_name,)
1866 1866
1867 1867 if user_group_name:
1868 1868 check_scope += ', user_group_name:%s' % (user_group_name,)
1869 1869
1870 1870 log.debug(
1871 1871 'checking cls:%s %s %s @ %s'
1872 1872 % (cls_name, self.required_perms, check_scope, check_location))
1873 1873 if not user:
1874 1874 log.debug('Empty User passed into arguments')
1875 1875 return False
1876 1876
1877 1877 # process user
1878 1878 if not isinstance(user, AuthUser):
1879 1879 user = AuthUser(user.user_id)
1880 1880 if not check_location:
1881 1881 check_location = 'unspecified'
1882 1882 if self.check_permissions(user.permissions, repo_name, group_name,
1883 1883 user_group_name):
1884 1884 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1885 1885 check_scope, user, check_location)
1886 1886 return True
1887 1887
1888 1888 else:
1889 1889 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1890 1890 check_scope, user, check_location)
1891 1891 return False
1892 1892
1893 1893 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1894 1894 user_group_name=None):
1895 1895 """
1896 1896 implement in child class should return True if permissions are ok,
1897 1897 False otherwise
1898 1898
1899 1899 :param perm_defs: dict with permission definitions
1900 1900 :param repo_name: repo name
1901 1901 """
1902 1902 raise NotImplementedError()
1903 1903
1904 1904
1905 1905 class HasPermissionAllApi(_BaseApiPerm):
1906 1906 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1907 1907 user_group_name=None):
1908 1908 if self.required_perms.issubset(perm_defs.get('global')):
1909 1909 return True
1910 1910 return False
1911 1911
1912 1912
1913 1913 class HasPermissionAnyApi(_BaseApiPerm):
1914 1914 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1915 1915 user_group_name=None):
1916 1916 if self.required_perms.intersection(perm_defs.get('global')):
1917 1917 return True
1918 1918 return False
1919 1919
1920 1920
1921 1921 class HasRepoPermissionAllApi(_BaseApiPerm):
1922 1922 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1923 1923 user_group_name=None):
1924 1924 try:
1925 1925 _user_perms = set([perm_defs['repositories'][repo_name]])
1926 1926 except KeyError:
1927 1927 log.warning(traceback.format_exc())
1928 1928 return False
1929 1929 if self.required_perms.issubset(_user_perms):
1930 1930 return True
1931 1931 return False
1932 1932
1933 1933
1934 1934 class HasRepoPermissionAnyApi(_BaseApiPerm):
1935 1935 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1936 1936 user_group_name=None):
1937 1937 try:
1938 1938 _user_perms = set([perm_defs['repositories'][repo_name]])
1939 1939 except KeyError:
1940 1940 log.warning(traceback.format_exc())
1941 1941 return False
1942 1942 if self.required_perms.intersection(_user_perms):
1943 1943 return True
1944 1944 return False
1945 1945
1946 1946
1947 1947 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1948 1948 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1949 1949 user_group_name=None):
1950 1950 try:
1951 1951 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1952 1952 except KeyError:
1953 1953 log.warning(traceback.format_exc())
1954 1954 return False
1955 1955 if self.required_perms.intersection(_user_perms):
1956 1956 return True
1957 1957 return False
1958 1958
1959 1959
1960 1960 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1961 1961 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1962 1962 user_group_name=None):
1963 1963 try:
1964 1964 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1965 1965 except KeyError:
1966 1966 log.warning(traceback.format_exc())
1967 1967 return False
1968 1968 if self.required_perms.issubset(_user_perms):
1969 1969 return True
1970 1970 return False
1971 1971
1972 1972
1973 1973 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1974 1974 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1975 1975 user_group_name=None):
1976 1976 try:
1977 1977 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1978 1978 except KeyError:
1979 1979 log.warning(traceback.format_exc())
1980 1980 return False
1981 1981 if self.required_perms.intersection(_user_perms):
1982 1982 return True
1983 1983 return False
1984 1984
1985 1985
1986 1986 def check_ip_access(source_ip, allowed_ips=None):
1987 1987 """
1988 1988 Checks if source_ip is a subnet of any of allowed_ips.
1989 1989
1990 1990 :param source_ip:
1991 1991 :param allowed_ips: list of allowed ips together with mask
1992 1992 """
1993 1993 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1994 1994 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
1995 1995 if isinstance(allowed_ips, (tuple, list, set)):
1996 1996 for ip in allowed_ips:
1997 1997 ip = safe_unicode(ip)
1998 1998 try:
1999 1999 network_address = ipaddress.ip_network(ip, strict=False)
2000 2000 if source_ip_address in network_address:
2001 2001 log.debug('IP %s is network %s' %
2002 2002 (source_ip_address, network_address))
2003 2003 return True
2004 2004 # for any case we cannot determine the IP, don't crash just
2005 2005 # skip it and log as error, we want to say forbidden still when
2006 2006 # sending bad IP
2007 2007 except Exception:
2008 2008 log.error(traceback.format_exc())
2009 2009 continue
2010 2010 return False
2011 2011
2012 2012
2013 2013 def get_cython_compat_decorator(wrapper, func):
2014 2014 """
2015 2015 Creates a cython compatible decorator. The previously used
2016 2016 decorator.decorator() function seems to be incompatible with cython.
2017 2017
2018 2018 :param wrapper: __wrapper method of the decorator class
2019 2019 :param func: decorated function
2020 2020 """
2021 2021 @wraps(func)
2022 2022 def local_wrapper(*args, **kwds):
2023 2023 return wrapper(func, *args, **kwds)
2024 2024 local_wrapper.__wrapped__ = func
2025 2025 return local_wrapper
2026 2026
2027 2027
@@ -1,2045 +1,2045 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 from collections import OrderedDict
40 40
41 41 import pygments
42 42 import itertools
43 43 import fnmatch
44 44
45 45 from datetime import datetime
46 46 from functools import partial
47 47 from pygments.formatters.html import HtmlFormatter
48 48 from pygments import highlight as code_highlight
49 49 from pygments.lexers import (
50 50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 51
52 52 from pyramid.threadlocal import get_current_request
53 53
54 54 from webhelpers.html import literal, HTML, escape
55 55 from webhelpers.html.tools import *
56 56 from webhelpers.html.builder import make_tag
57 57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 63 from webhelpers.pylonslib import Flash as _Flash
64 64 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
65 65 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
66 66 replace_whitespace, urlify, truncate, wrap_paragraphs
67 67 from webhelpers.date import time_ago_in_words
68 68 from webhelpers.paginate import Page as _Page
69 69 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
70 70 convert_boolean_attrs, NotGiven, _make_safe_id_component
71 71 from webhelpers2.number import format_byte_size
72 72
73 73 from rhodecode.lib.action_parser import action_parser
74 74 from rhodecode.lib.ext_json import json
75 75 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
76 76 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
77 77 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
78 78 AttributeDict, safe_int, md5, md5_safe
79 79 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
80 80 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
81 81 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
82 82 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
83 83 from rhodecode.model.changeset_status import ChangesetStatusModel
84 84 from rhodecode.model.db import Permission, User, Repository
85 85 from rhodecode.model.repo_group import RepoGroupModel
86 86 from rhodecode.model.settings import IssueTrackerSettingsModel
87 87
88 88 log = logging.getLogger(__name__)
89 89
90 90
91 91 DEFAULT_USER = User.DEFAULT_USER
92 92 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
93 93
94 94
95 95 def url(*args, **kw):
96 96 from pylons import url as pylons_url
97 97 return pylons_url(*args, **kw)
98 98
99 99
100 100 def pylons_url_current(*args, **kw):
101 101 """
102 102 This function overrides pylons.url.current() which returns the current
103 103 path so that it will also work from a pyramid only context. This
104 104 should be removed once port to pyramid is complete.
105 105 """
106 106 from pylons import url as pylons_url
107 107 if not args and not kw:
108 108 request = get_current_request()
109 109 return request.path
110 110 return pylons_url.current(*args, **kw)
111 111
112 112 url.current = pylons_url_current
113 113
114 114
115 115 def url_replace(**qargs):
116 116 """ Returns the current request url while replacing query string args """
117 117
118 118 request = get_current_request()
119 119 new_args = request.GET.mixed()
120 120 new_args.update(qargs)
121 121 return url('', **new_args)
122 122
123 123
124 124 def asset(path, ver=None, **kwargs):
125 125 """
126 126 Helper to generate a static asset file path for rhodecode assets
127 127
128 128 eg. h.asset('images/image.png', ver='3923')
129 129
130 130 :param path: path of asset
131 131 :param ver: optional version query param to append as ?ver=
132 132 """
133 133 request = get_current_request()
134 134 query = {}
135 135 query.update(kwargs)
136 136 if ver:
137 137 query = {'ver': ver}
138 138 return request.static_path(
139 139 'rhodecode:public/{}'.format(path), _query=query)
140 140
141 141
142 142 default_html_escape_table = {
143 143 ord('&'): u'&amp;',
144 144 ord('<'): u'&lt;',
145 145 ord('>'): u'&gt;',
146 146 ord('"'): u'&quot;',
147 147 ord("'"): u'&#39;',
148 148 }
149 149
150 150
151 151 def html_escape(text, html_escape_table=default_html_escape_table):
152 152 """Produce entities within text."""
153 153 return text.translate(html_escape_table)
154 154
155 155
156 156 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
157 157 """
158 158 Truncate string ``s`` at the first occurrence of ``sub``.
159 159
160 160 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
161 161 """
162 162 suffix_if_chopped = suffix_if_chopped or ''
163 163 pos = s.find(sub)
164 164 if pos == -1:
165 165 return s
166 166
167 167 if inclusive:
168 168 pos += len(sub)
169 169
170 170 chopped = s[:pos]
171 171 left = s[pos:].strip()
172 172
173 173 if left and suffix_if_chopped:
174 174 chopped += suffix_if_chopped
175 175
176 176 return chopped
177 177
178 178
179 179 def shorter(text, size=20):
180 180 postfix = '...'
181 181 if len(text) > size:
182 182 return text[:size - len(postfix)] + postfix
183 183 return text
184 184
185 185
186 186 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
187 187 """
188 188 Reset button
189 189 """
190 190 _set_input_attrs(attrs, type, name, value)
191 191 _set_id_attr(attrs, id, name)
192 192 convert_boolean_attrs(attrs, ["disabled"])
193 193 return HTML.input(**attrs)
194 194
195 195 reset = _reset
196 196 safeid = _make_safe_id_component
197 197
198 198
199 199 def branding(name, length=40):
200 200 return truncate(name, length, indicator="")
201 201
202 202
203 203 def FID(raw_id, path):
204 204 """
205 205 Creates a unique ID for filenode based on it's hash of path and commit
206 206 it's safe to use in urls
207 207
208 208 :param raw_id:
209 209 :param path:
210 210 """
211 211
212 212 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
213 213
214 214
215 215 class _GetError(object):
216 216 """Get error from form_errors, and represent it as span wrapped error
217 217 message
218 218
219 219 :param field_name: field to fetch errors for
220 220 :param form_errors: form errors dict
221 221 """
222 222
223 223 def __call__(self, field_name, form_errors):
224 224 tmpl = """<span class="error_msg">%s</span>"""
225 225 if form_errors and field_name in form_errors:
226 226 return literal(tmpl % form_errors.get(field_name))
227 227
228 228 get_error = _GetError()
229 229
230 230
231 231 class _ToolTip(object):
232 232
233 233 def __call__(self, tooltip_title, trim_at=50):
234 234 """
235 235 Special function just to wrap our text into nice formatted
236 236 autowrapped text
237 237
238 238 :param tooltip_title:
239 239 """
240 240 tooltip_title = escape(tooltip_title)
241 241 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
242 242 return tooltip_title
243 243 tooltip = _ToolTip()
244 244
245 245
246 246 def files_breadcrumbs(repo_name, commit_id, file_path):
247 247 if isinstance(file_path, str):
248 248 file_path = safe_unicode(file_path)
249 249
250 250 # TODO: johbo: Is this always a url like path, or is this operating
251 251 # system dependent?
252 252 path_segments = file_path.split('/')
253 253
254 254 repo_name_html = escape(repo_name)
255 255 if len(path_segments) == 1 and path_segments[0] == '':
256 256 url_segments = [repo_name_html]
257 257 else:
258 258 url_segments = [
259 259 link_to(
260 260 repo_name_html,
261 261 route_path(
262 262 'repo_files',
263 263 repo_name=repo_name,
264 264 commit_id=commit_id,
265 265 f_path=''),
266 266 class_='pjax-link')]
267 267
268 268 last_cnt = len(path_segments) - 1
269 269 for cnt, segment in enumerate(path_segments):
270 270 if not segment:
271 271 continue
272 272 segment_html = escape(segment)
273 273
274 274 if cnt != last_cnt:
275 275 url_segments.append(
276 276 link_to(
277 277 segment_html,
278 278 route_path(
279 279 'repo_files',
280 280 repo_name=repo_name,
281 281 commit_id=commit_id,
282 282 f_path='/'.join(path_segments[:cnt + 1])),
283 283 class_='pjax-link'))
284 284 else:
285 285 url_segments.append(segment_html)
286 286
287 287 return literal('/'.join(url_segments))
288 288
289 289
290 290 class CodeHtmlFormatter(HtmlFormatter):
291 291 """
292 292 My code Html Formatter for source codes
293 293 """
294 294
295 295 def wrap(self, source, outfile):
296 296 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
297 297
298 298 def _wrap_code(self, source):
299 299 for cnt, it in enumerate(source):
300 300 i, t = it
301 301 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
302 302 yield i, t
303 303
304 304 def _wrap_tablelinenos(self, inner):
305 305 dummyoutfile = StringIO.StringIO()
306 306 lncount = 0
307 307 for t, line in inner:
308 308 if t:
309 309 lncount += 1
310 310 dummyoutfile.write(line)
311 311
312 312 fl = self.linenostart
313 313 mw = len(str(lncount + fl - 1))
314 314 sp = self.linenospecial
315 315 st = self.linenostep
316 316 la = self.lineanchors
317 317 aln = self.anchorlinenos
318 318 nocls = self.noclasses
319 319 if sp:
320 320 lines = []
321 321
322 322 for i in range(fl, fl + lncount):
323 323 if i % st == 0:
324 324 if i % sp == 0:
325 325 if aln:
326 326 lines.append('<a href="#%s%d" class="special">%*d</a>' %
327 327 (la, i, mw, i))
328 328 else:
329 329 lines.append('<span class="special">%*d</span>' % (mw, i))
330 330 else:
331 331 if aln:
332 332 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
333 333 else:
334 334 lines.append('%*d' % (mw, i))
335 335 else:
336 336 lines.append('')
337 337 ls = '\n'.join(lines)
338 338 else:
339 339 lines = []
340 340 for i in range(fl, fl + lncount):
341 341 if i % st == 0:
342 342 if aln:
343 343 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
344 344 else:
345 345 lines.append('%*d' % (mw, i))
346 346 else:
347 347 lines.append('')
348 348 ls = '\n'.join(lines)
349 349
350 350 # in case you wonder about the seemingly redundant <div> here: since the
351 351 # content in the other cell also is wrapped in a div, some browsers in
352 352 # some configurations seem to mess up the formatting...
353 353 if nocls:
354 354 yield 0, ('<table class="%stable">' % self.cssclass +
355 355 '<tr><td><div class="linenodiv" '
356 356 'style="background-color: #f0f0f0; padding-right: 10px">'
357 357 '<pre style="line-height: 125%">' +
358 358 ls + '</pre></div></td><td id="hlcode" class="code">')
359 359 else:
360 360 yield 0, ('<table class="%stable">' % self.cssclass +
361 361 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
362 362 ls + '</pre></div></td><td id="hlcode" class="code">')
363 363 yield 0, dummyoutfile.getvalue()
364 364 yield 0, '</td></tr></table>'
365 365
366 366
367 367 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
368 368 def __init__(self, **kw):
369 369 # only show these line numbers if set
370 370 self.only_lines = kw.pop('only_line_numbers', [])
371 371 self.query_terms = kw.pop('query_terms', [])
372 372 self.max_lines = kw.pop('max_lines', 5)
373 373 self.line_context = kw.pop('line_context', 3)
374 374 self.url = kw.pop('url', None)
375 375
376 376 super(CodeHtmlFormatter, self).__init__(**kw)
377 377
378 378 def _wrap_code(self, source):
379 379 for cnt, it in enumerate(source):
380 380 i, t = it
381 381 t = '<pre>%s</pre>' % t
382 382 yield i, t
383 383
384 384 def _wrap_tablelinenos(self, inner):
385 385 yield 0, '<table class="code-highlight %stable">' % self.cssclass
386 386
387 387 last_shown_line_number = 0
388 388 current_line_number = 1
389 389
390 390 for t, line in inner:
391 391 if not t:
392 392 yield t, line
393 393 continue
394 394
395 395 if current_line_number in self.only_lines:
396 396 if last_shown_line_number + 1 != current_line_number:
397 397 yield 0, '<tr>'
398 398 yield 0, '<td class="line">...</td>'
399 399 yield 0, '<td id="hlcode" class="code"></td>'
400 400 yield 0, '</tr>'
401 401
402 402 yield 0, '<tr>'
403 403 if self.url:
404 404 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
405 405 self.url, current_line_number, current_line_number)
406 406 else:
407 407 yield 0, '<td class="line"><a href="">%i</a></td>' % (
408 408 current_line_number)
409 409 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
410 410 yield 0, '</tr>'
411 411
412 412 last_shown_line_number = current_line_number
413 413
414 414 current_line_number += 1
415 415
416 416
417 417 yield 0, '</table>'
418 418
419 419
420 420 def extract_phrases(text_query):
421 421 """
422 422 Extracts phrases from search term string making sure phrases
423 423 contained in double quotes are kept together - and discarding empty values
424 424 or fully whitespace values eg.
425 425
426 426 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
427 427
428 428 """
429 429
430 430 in_phrase = False
431 431 buf = ''
432 432 phrases = []
433 433 for char in text_query:
434 434 if in_phrase:
435 435 if char == '"': # end phrase
436 436 phrases.append(buf)
437 437 buf = ''
438 438 in_phrase = False
439 439 continue
440 440 else:
441 441 buf += char
442 442 continue
443 443 else:
444 444 if char == '"': # start phrase
445 445 in_phrase = True
446 446 phrases.append(buf)
447 447 buf = ''
448 448 continue
449 449 elif char == ' ':
450 450 phrases.append(buf)
451 451 buf = ''
452 452 continue
453 453 else:
454 454 buf += char
455 455
456 456 phrases.append(buf)
457 457 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
458 458 return phrases
459 459
460 460
461 461 def get_matching_offsets(text, phrases):
462 462 """
463 463 Returns a list of string offsets in `text` that the list of `terms` match
464 464
465 465 >>> get_matching_offsets('some text here', ['some', 'here'])
466 466 [(0, 4), (10, 14)]
467 467
468 468 """
469 469 offsets = []
470 470 for phrase in phrases:
471 471 for match in re.finditer(phrase, text):
472 472 offsets.append((match.start(), match.end()))
473 473
474 474 return offsets
475 475
476 476
477 477 def normalize_text_for_matching(x):
478 478 """
479 479 Replaces all non alnum characters to spaces and lower cases the string,
480 480 useful for comparing two text strings without punctuation
481 481 """
482 482 return re.sub(r'[^\w]', ' ', x.lower())
483 483
484 484
485 485 def get_matching_line_offsets(lines, terms):
486 486 """ Return a set of `lines` indices (starting from 1) matching a
487 487 text search query, along with `context` lines above/below matching lines
488 488
489 489 :param lines: list of strings representing lines
490 490 :param terms: search term string to match in lines eg. 'some text'
491 491 :param context: number of lines above/below a matching line to add to result
492 492 :param max_lines: cut off for lines of interest
493 493 eg.
494 494
495 495 text = '''
496 496 words words words
497 497 words words words
498 498 some text some
499 499 words words words
500 500 words words words
501 501 text here what
502 502 '''
503 503 get_matching_line_offsets(text, 'text', context=1)
504 504 {3: [(5, 9)], 6: [(0, 4)]]
505 505
506 506 """
507 507 matching_lines = {}
508 508 phrases = [normalize_text_for_matching(phrase)
509 509 for phrase in extract_phrases(terms)]
510 510
511 511 for line_index, line in enumerate(lines, start=1):
512 512 match_offsets = get_matching_offsets(
513 513 normalize_text_for_matching(line), phrases)
514 514 if match_offsets:
515 515 matching_lines[line_index] = match_offsets
516 516
517 517 return matching_lines
518 518
519 519
520 520 def hsv_to_rgb(h, s, v):
521 521 """ Convert hsv color values to rgb """
522 522
523 523 if s == 0.0:
524 524 return v, v, v
525 525 i = int(h * 6.0) # XXX assume int() truncates!
526 526 f = (h * 6.0) - i
527 527 p = v * (1.0 - s)
528 528 q = v * (1.0 - s * f)
529 529 t = v * (1.0 - s * (1.0 - f))
530 530 i = i % 6
531 531 if i == 0:
532 532 return v, t, p
533 533 if i == 1:
534 534 return q, v, p
535 535 if i == 2:
536 536 return p, v, t
537 537 if i == 3:
538 538 return p, q, v
539 539 if i == 4:
540 540 return t, p, v
541 541 if i == 5:
542 542 return v, p, q
543 543
544 544
545 545 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
546 546 """
547 547 Generator for getting n of evenly distributed colors using
548 548 hsv color and golden ratio. It always return same order of colors
549 549
550 550 :param n: number of colors to generate
551 551 :param saturation: saturation of returned colors
552 552 :param lightness: lightness of returned colors
553 553 :returns: RGB tuple
554 554 """
555 555
556 556 golden_ratio = 0.618033988749895
557 557 h = 0.22717784590367374
558 558
559 559 for _ in xrange(n):
560 560 h += golden_ratio
561 561 h %= 1
562 562 HSV_tuple = [h, saturation, lightness]
563 563 RGB_tuple = hsv_to_rgb(*HSV_tuple)
564 564 yield map(lambda x: str(int(x * 256)), RGB_tuple)
565 565
566 566
567 567 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
568 568 """
569 569 Returns a function which when called with an argument returns a unique
570 570 color for that argument, eg.
571 571
572 572 :param n: number of colors to generate
573 573 :param saturation: saturation of returned colors
574 574 :param lightness: lightness of returned colors
575 575 :returns: css RGB string
576 576
577 577 >>> color_hash = color_hasher()
578 578 >>> color_hash('hello')
579 579 'rgb(34, 12, 59)'
580 580 >>> color_hash('hello')
581 581 'rgb(34, 12, 59)'
582 582 >>> color_hash('other')
583 583 'rgb(90, 224, 159)'
584 584 """
585 585
586 586 color_dict = {}
587 587 cgenerator = unique_color_generator(
588 588 saturation=saturation, lightness=lightness)
589 589
590 590 def get_color_string(thing):
591 591 if thing in color_dict:
592 592 col = color_dict[thing]
593 593 else:
594 594 col = color_dict[thing] = cgenerator.next()
595 595 return "rgb(%s)" % (', '.join(col))
596 596
597 597 return get_color_string
598 598
599 599
600 600 def get_lexer_safe(mimetype=None, filepath=None):
601 601 """
602 602 Tries to return a relevant pygments lexer using mimetype/filepath name,
603 603 defaulting to plain text if none could be found
604 604 """
605 605 lexer = None
606 606 try:
607 607 if mimetype:
608 608 lexer = get_lexer_for_mimetype(mimetype)
609 609 if not lexer:
610 610 lexer = get_lexer_for_filename(filepath)
611 611 except pygments.util.ClassNotFound:
612 612 pass
613 613
614 614 if not lexer:
615 615 lexer = get_lexer_by_name('text')
616 616
617 617 return lexer
618 618
619 619
620 620 def get_lexer_for_filenode(filenode):
621 621 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
622 622 return lexer
623 623
624 624
625 625 def pygmentize(filenode, **kwargs):
626 626 """
627 627 pygmentize function using pygments
628 628
629 629 :param filenode:
630 630 """
631 631 lexer = get_lexer_for_filenode(filenode)
632 632 return literal(code_highlight(filenode.content, lexer,
633 633 CodeHtmlFormatter(**kwargs)))
634 634
635 635
636 636 def is_following_repo(repo_name, user_id):
637 637 from rhodecode.model.scm import ScmModel
638 638 return ScmModel().is_following_repo(repo_name, user_id)
639 639
640 640
641 641 class _Message(object):
642 642 """A message returned by ``Flash.pop_messages()``.
643 643
644 644 Converting the message to a string returns the message text. Instances
645 645 also have the following attributes:
646 646
647 647 * ``message``: the message text.
648 648 * ``category``: the category specified when the message was created.
649 649 """
650 650
651 651 def __init__(self, category, message):
652 652 self.category = category
653 653 self.message = message
654 654
655 655 def __str__(self):
656 656 return self.message
657 657
658 658 __unicode__ = __str__
659 659
660 660 def __html__(self):
661 661 return escape(safe_unicode(self.message))
662 662
663 663
664 664 class Flash(_Flash):
665 665
666 666 def pop_messages(self, request=None):
667 667 """Return all accumulated messages and delete them from the session.
668 668
669 669 The return value is a list of ``Message`` objects.
670 670 """
671 671 messages = []
672 672
673 673 if request:
674 674 session = request.session
675 675 else:
676 676 from pylons import session
677 677
678 678 # Pop the 'old' pylons flash messages. They are tuples of the form
679 679 # (category, message)
680 680 for cat, msg in session.pop(self.session_key, []):
681 681 messages.append(_Message(cat, msg))
682 682
683 683 # Pop the 'new' pyramid flash messages for each category as list
684 684 # of strings.
685 685 for cat in self.categories:
686 686 for msg in session.pop_flash(queue=cat):
687 687 messages.append(_Message(cat, msg))
688 688 # Map messages from the default queue to the 'notice' category.
689 689 for msg in session.pop_flash():
690 690 messages.append(_Message('notice', msg))
691 691
692 692 session.save()
693 693 return messages
694 694
695 695 def json_alerts(self, request=None):
696 696 payloads = []
697 697 messages = flash.pop_messages(request=request)
698 698 if messages:
699 699 for message in messages:
700 700 subdata = {}
701 701 if hasattr(message.message, 'rsplit'):
702 702 flash_data = message.message.rsplit('|DELIM|', 1)
703 703 org_message = flash_data[0]
704 704 if len(flash_data) > 1:
705 705 subdata = json.loads(flash_data[1])
706 706 else:
707 707 org_message = message.message
708 708 payloads.append({
709 709 'message': {
710 710 'message': u'{}'.format(org_message),
711 711 'level': message.category,
712 712 'force': True,
713 713 'subdata': subdata
714 714 }
715 715 })
716 716 return json.dumps(payloads)
717 717
718 718 flash = Flash()
719 719
720 720 #==============================================================================
721 721 # SCM FILTERS available via h.
722 722 #==============================================================================
723 723 from rhodecode.lib.vcs.utils import author_name, author_email
724 724 from rhodecode.lib.utils2 import credentials_filter, age as _age
725 725 from rhodecode.model.db import User, ChangesetStatus
726 726
727 727 age = _age
728 728 capitalize = lambda x: x.capitalize()
729 729 email = author_email
730 730 short_id = lambda x: x[:12]
731 731 hide_credentials = lambda x: ''.join(credentials_filter(x))
732 732
733 733
734 734 def age_component(datetime_iso, value=None, time_is_local=False):
735 735 title = value or format_date(datetime_iso)
736 736 tzinfo = '+00:00'
737 737
738 738 # detect if we have a timezone info, otherwise, add it
739 739 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
740 740 if time_is_local:
741 741 tzinfo = time.strftime("+%H:%M",
742 742 time.gmtime(
743 743 (datetime.now() - datetime.utcnow()).seconds + 1
744 744 )
745 745 )
746 746
747 747 return literal(
748 748 '<time class="timeago tooltip" '
749 749 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
750 750 datetime_iso, title, tzinfo))
751 751
752 752
753 753 def _shorten_commit_id(commit_id):
754 754 from rhodecode import CONFIG
755 755 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
756 756 return commit_id[:def_len]
757 757
758 758
759 759 def show_id(commit):
760 760 """
761 761 Configurable function that shows ID
762 762 by default it's r123:fffeeefffeee
763 763
764 764 :param commit: commit instance
765 765 """
766 766 from rhodecode import CONFIG
767 767 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
768 768
769 769 raw_id = _shorten_commit_id(commit.raw_id)
770 770 if show_idx:
771 771 return 'r%s:%s' % (commit.idx, raw_id)
772 772 else:
773 773 return '%s' % (raw_id, )
774 774
775 775
776 776 def format_date(date):
777 777 """
778 778 use a standardized formatting for dates used in RhodeCode
779 779
780 780 :param date: date/datetime object
781 781 :return: formatted date
782 782 """
783 783
784 784 if date:
785 785 _fmt = "%a, %d %b %Y %H:%M:%S"
786 786 return safe_unicode(date.strftime(_fmt))
787 787
788 788 return u""
789 789
790 790
791 791 class _RepoChecker(object):
792 792
793 793 def __init__(self, backend_alias):
794 794 self._backend_alias = backend_alias
795 795
796 796 def __call__(self, repository):
797 797 if hasattr(repository, 'alias'):
798 798 _type = repository.alias
799 799 elif hasattr(repository, 'repo_type'):
800 800 _type = repository.repo_type
801 801 else:
802 802 _type = repository
803 803 return _type == self._backend_alias
804 804
805 805 is_git = _RepoChecker('git')
806 806 is_hg = _RepoChecker('hg')
807 807 is_svn = _RepoChecker('svn')
808 808
809 809
810 810 def get_repo_type_by_name(repo_name):
811 811 repo = Repository.get_by_repo_name(repo_name)
812 812 return repo.repo_type
813 813
814 814
815 815 def is_svn_without_proxy(repository):
816 816 if is_svn(repository):
817 817 from rhodecode.model.settings import VcsSettingsModel
818 818 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
819 819 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
820 820 return False
821 821
822 822
823 823 def discover_user(author):
824 824 """
825 825 Tries to discover RhodeCode User based on the autho string. Author string
826 826 is typically `FirstName LastName <email@address.com>`
827 827 """
828 828
829 829 # if author is already an instance use it for extraction
830 830 if isinstance(author, User):
831 831 return author
832 832
833 833 # Valid email in the attribute passed, see if they're in the system
834 834 _email = author_email(author)
835 835 if _email != '':
836 836 user = User.get_by_email(_email, case_insensitive=True, cache=True)
837 837 if user is not None:
838 838 return user
839 839
840 840 # Maybe it's a username, we try to extract it and fetch by username ?
841 841 _author = author_name(author)
842 842 user = User.get_by_username(_author, case_insensitive=True, cache=True)
843 843 if user is not None:
844 844 return user
845 845
846 846 return None
847 847
848 848
849 849 def email_or_none(author):
850 850 # extract email from the commit string
851 851 _email = author_email(author)
852 852
853 853 # If we have an email, use it, otherwise
854 854 # see if it contains a username we can get an email from
855 855 if _email != '':
856 856 return _email
857 857 else:
858 858 user = User.get_by_username(
859 859 author_name(author), case_insensitive=True, cache=True)
860 860
861 861 if user is not None:
862 862 return user.email
863 863
864 864 # No valid email, not a valid user in the system, none!
865 865 return None
866 866
867 867
868 868 def link_to_user(author, length=0, **kwargs):
869 869 user = discover_user(author)
870 870 # user can be None, but if we have it already it means we can re-use it
871 871 # in the person() function, so we save 1 intensive-query
872 872 if user:
873 873 author = user
874 874
875 875 display_person = person(author, 'username_or_name_or_email')
876 876 if length:
877 877 display_person = shorter(display_person, length)
878 878
879 879 if user:
880 880 return link_to(
881 881 escape(display_person),
882 882 route_path('user_profile', username=user.username),
883 883 **kwargs)
884 884 else:
885 885 return escape(display_person)
886 886
887 887
888 888 def person(author, show_attr="username_and_name"):
889 889 user = discover_user(author)
890 890 if user:
891 891 return getattr(user, show_attr)
892 892 else:
893 893 _author = author_name(author)
894 894 _email = email(author)
895 895 return _author or _email
896 896
897 897
898 898 def author_string(email):
899 899 if email:
900 900 user = User.get_by_email(email, case_insensitive=True, cache=True)
901 901 if user:
902 902 if user.first_name or user.last_name:
903 903 return '%s %s &lt;%s&gt;' % (
904 904 user.first_name, user.last_name, email)
905 905 else:
906 906 return email
907 907 else:
908 908 return email
909 909 else:
910 910 return None
911 911
912 912
913 913 def person_by_id(id_, show_attr="username_and_name"):
914 914 # attr to return from fetched user
915 915 person_getter = lambda usr: getattr(usr, show_attr)
916 916
917 917 #maybe it's an ID ?
918 918 if str(id_).isdigit() or isinstance(id_, int):
919 919 id_ = int(id_)
920 920 user = User.get(id_)
921 921 if user is not None:
922 922 return person_getter(user)
923 923 return id_
924 924
925 925
926 926 def gravatar_with_user(request, author, show_disabled=False):
927 927 _render = request.get_partial_renderer('base/base.mako')
928 928 return _render('gravatar_with_user', author, show_disabled=show_disabled)
929 929
930 930
931 931 def desc_stylize(value):
932 932 """
933 933 converts tags from value into html equivalent
934 934
935 935 :param value:
936 936 """
937 937 if not value:
938 938 return ''
939 939
940 940 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
941 941 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
942 942 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
943 943 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
944 944 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
945 945 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
946 946 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
947 947 '<div class="metatag" tag="lang">\\2</div>', value)
948 948 value = re.sub(r'\[([a-z]+)\]',
949 949 '<div class="metatag" tag="\\1">\\1</div>', value)
950 950
951 951 return value
952 952
953 953
954 954 def escaped_stylize(value):
955 955 """
956 956 converts tags from value into html equivalent, but escaping its value first
957 957 """
958 958 if not value:
959 959 return ''
960 960
961 961 # Using default webhelper escape method, but has to force it as a
962 962 # plain unicode instead of a markup tag to be used in regex expressions
963 963 value = unicode(escape(safe_unicode(value)))
964 964
965 965 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
966 966 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
967 967 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
968 968 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
969 969 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
970 970 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
971 971 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
972 972 '<div class="metatag" tag="lang">\\2</div>', value)
973 973 value = re.sub(r'\[([a-z]+)\]',
974 974 '<div class="metatag" tag="\\1">\\1</div>', value)
975 975
976 976 return value
977 977
978 978
979 979 def bool2icon(value):
980 980 """
981 981 Returns boolean value of a given value, represented as html element with
982 982 classes that will represent icons
983 983
984 984 :param value: given value to convert to html node
985 985 """
986 986
987 987 if value: # does bool conversion
988 988 return HTML.tag('i', class_="icon-true")
989 989 else: # not true as bool
990 990 return HTML.tag('i', class_="icon-false")
991 991
992 992
993 993 #==============================================================================
994 994 # PERMS
995 995 #==============================================================================
996 996 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
997 997 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
998 998 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
999 999 csrf_token_key
1000 1000
1001 1001
1002 1002 #==============================================================================
1003 1003 # GRAVATAR URL
1004 1004 #==============================================================================
1005 1005 class InitialsGravatar(object):
1006 1006 def __init__(self, email_address, first_name, last_name, size=30,
1007 1007 background=None, text_color='#fff'):
1008 1008 self.size = size
1009 1009 self.first_name = first_name
1010 1010 self.last_name = last_name
1011 1011 self.email_address = email_address
1012 1012 self.background = background or self.str2color(email_address)
1013 1013 self.text_color = text_color
1014 1014
1015 1015 def get_color_bank(self):
1016 1016 """
1017 1017 returns a predefined list of colors that gravatars can use.
1018 1018 Those are randomized distinct colors that guarantee readability and
1019 1019 uniqueness.
1020 1020
1021 1021 generated with: http://phrogz.net/css/distinct-colors.html
1022 1022 """
1023 1023 return [
1024 1024 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1025 1025 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1026 1026 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1027 1027 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1028 1028 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1029 1029 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1030 1030 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1031 1031 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1032 1032 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1033 1033 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1034 1034 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1035 1035 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1036 1036 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1037 1037 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1038 1038 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1039 1039 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1040 1040 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1041 1041 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1042 1042 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1043 1043 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1044 1044 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1045 1045 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1046 1046 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1047 1047 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1048 1048 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1049 1049 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1050 1050 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1051 1051 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1052 1052 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1053 1053 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1054 1054 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1055 1055 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1056 1056 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1057 1057 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1058 1058 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1059 1059 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1060 1060 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1061 1061 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1062 1062 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1063 1063 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1064 1064 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1065 1065 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1066 1066 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1067 1067 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1068 1068 '#4f8c46', '#368dd9', '#5c0073'
1069 1069 ]
1070 1070
1071 1071 def rgb_to_hex_color(self, rgb_tuple):
1072 1072 """
1073 1073 Converts an rgb_tuple passed to an hex color.
1074 1074
1075 1075 :param rgb_tuple: tuple with 3 ints represents rgb color space
1076 1076 """
1077 1077 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1078 1078
1079 1079 def email_to_int_list(self, email_str):
1080 1080 """
1081 1081 Get every byte of the hex digest value of email and turn it to integer.
1082 1082 It's going to be always between 0-255
1083 1083 """
1084 1084 digest = md5_safe(email_str.lower())
1085 1085 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1086 1086
1087 1087 def pick_color_bank_index(self, email_str, color_bank):
1088 1088 return self.email_to_int_list(email_str)[0] % len(color_bank)
1089 1089
1090 1090 def str2color(self, email_str):
1091 1091 """
1092 1092 Tries to map in a stable algorithm an email to color
1093 1093
1094 1094 :param email_str:
1095 1095 """
1096 1096 color_bank = self.get_color_bank()
1097 1097 # pick position (module it's length so we always find it in the
1098 1098 # bank even if it's smaller than 256 values
1099 1099 pos = self.pick_color_bank_index(email_str, color_bank)
1100 1100 return color_bank[pos]
1101 1101
1102 1102 def normalize_email(self, email_address):
1103 1103 import unicodedata
1104 1104 # default host used to fill in the fake/missing email
1105 1105 default_host = u'localhost'
1106 1106
1107 1107 if not email_address:
1108 1108 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1109 1109
1110 1110 email_address = safe_unicode(email_address)
1111 1111
1112 1112 if u'@' not in email_address:
1113 1113 email_address = u'%s@%s' % (email_address, default_host)
1114 1114
1115 1115 if email_address.endswith(u'@'):
1116 1116 email_address = u'%s%s' % (email_address, default_host)
1117 1117
1118 1118 email_address = unicodedata.normalize('NFKD', email_address)\
1119 1119 .encode('ascii', 'ignore')
1120 1120 return email_address
1121 1121
1122 1122 def get_initials(self):
1123 1123 """
1124 1124 Returns 2 letter initials calculated based on the input.
1125 1125 The algorithm picks first given email address, and takes first letter
1126 1126 of part before @, and then the first letter of server name. In case
1127 1127 the part before @ is in a format of `somestring.somestring2` it replaces
1128 1128 the server letter with first letter of somestring2
1129 1129
1130 1130 In case function was initialized with both first and lastname, this
1131 1131 overrides the extraction from email by first letter of the first and
1132 1132 last name. We add special logic to that functionality, In case Full name
1133 1133 is compound, like Guido Von Rossum, we use last part of the last name
1134 1134 (Von Rossum) picking `R`.
1135 1135
1136 1136 Function also normalizes the non-ascii characters to they ascii
1137 1137 representation, eg Ą => A
1138 1138 """
1139 1139 import unicodedata
1140 1140 # replace non-ascii to ascii
1141 1141 first_name = unicodedata.normalize(
1142 1142 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1143 1143 last_name = unicodedata.normalize(
1144 1144 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1145 1145
1146 1146 # do NFKD encoding, and also make sure email has proper format
1147 1147 email_address = self.normalize_email(self.email_address)
1148 1148
1149 1149 # first push the email initials
1150 1150 prefix, server = email_address.split('@', 1)
1151 1151
1152 1152 # check if prefix is maybe a 'first_name.last_name' syntax
1153 1153 _dot_split = prefix.rsplit('.', 1)
1154 1154 if len(_dot_split) == 2:
1155 1155 initials = [_dot_split[0][0], _dot_split[1][0]]
1156 1156 else:
1157 1157 initials = [prefix[0], server[0]]
1158 1158
1159 1159 # then try to replace either first_name or last_name
1160 1160 fn_letter = (first_name or " ")[0].strip()
1161 1161 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1162 1162
1163 1163 if fn_letter:
1164 1164 initials[0] = fn_letter
1165 1165
1166 1166 if ln_letter:
1167 1167 initials[1] = ln_letter
1168 1168
1169 1169 return ''.join(initials).upper()
1170 1170
1171 1171 def get_img_data_by_type(self, font_family, img_type):
1172 1172 default_user = """
1173 1173 <svg xmlns="http://www.w3.org/2000/svg"
1174 1174 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1175 1175 viewBox="-15 -10 439.165 429.164"
1176 1176
1177 1177 xml:space="preserve"
1178 1178 style="background:{background};" >
1179 1179
1180 1180 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1181 1181 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1182 1182 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1183 1183 168.596,153.916,216.671,
1184 1184 204.583,216.671z" fill="{text_color}"/>
1185 1185 <path d="M407.164,374.717L360.88,
1186 1186 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1187 1187 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1188 1188 15.366-44.203,23.488-69.076,23.488c-24.877,
1189 1189 0-48.762-8.122-69.078-23.488
1190 1190 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1191 1191 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1192 1192 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1193 1193 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1194 1194 19.402-10.527 C409.699,390.129,
1195 1195 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1196 1196 </svg>""".format(
1197 1197 size=self.size,
1198 1198 background='#979797', # @grey4
1199 1199 text_color=self.text_color,
1200 1200 font_family=font_family)
1201 1201
1202 1202 return {
1203 1203 "default_user": default_user
1204 1204 }[img_type]
1205 1205
1206 1206 def get_img_data(self, svg_type=None):
1207 1207 """
1208 1208 generates the svg metadata for image
1209 1209 """
1210 1210
1211 1211 font_family = ','.join([
1212 1212 'proximanovaregular',
1213 1213 'Proxima Nova Regular',
1214 1214 'Proxima Nova',
1215 1215 'Arial',
1216 1216 'Lucida Grande',
1217 1217 'sans-serif'
1218 1218 ])
1219 1219 if svg_type:
1220 1220 return self.get_img_data_by_type(font_family, svg_type)
1221 1221
1222 1222 initials = self.get_initials()
1223 1223 img_data = """
1224 1224 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1225 1225 width="{size}" height="{size}"
1226 1226 style="width: 100%; height: 100%; background-color: {background}"
1227 1227 viewBox="0 0 {size} {size}">
1228 1228 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1229 1229 pointer-events="auto" fill="{text_color}"
1230 1230 font-family="{font_family}"
1231 1231 style="font-weight: 400; font-size: {f_size}px;">{text}
1232 1232 </text>
1233 1233 </svg>""".format(
1234 1234 size=self.size,
1235 1235 f_size=self.size/1.85, # scale the text inside the box nicely
1236 1236 background=self.background,
1237 1237 text_color=self.text_color,
1238 1238 text=initials.upper(),
1239 1239 font_family=font_family)
1240 1240
1241 1241 return img_data
1242 1242
1243 1243 def generate_svg(self, svg_type=None):
1244 1244 img_data = self.get_img_data(svg_type)
1245 1245 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1246 1246
1247 1247
1248 1248 def initials_gravatar(email_address, first_name, last_name, size=30):
1249 1249 svg_type = None
1250 1250 if email_address == User.DEFAULT_USER_EMAIL:
1251 1251 svg_type = 'default_user'
1252 1252 klass = InitialsGravatar(email_address, first_name, last_name, size)
1253 1253 return klass.generate_svg(svg_type=svg_type)
1254 1254
1255 1255
1256 1256 def gravatar_url(email_address, size=30, request=None):
1257 1257 request = get_current_request()
1258 1258 if request and hasattr(request, 'call_context'):
1259 1259 _use_gravatar = request.call_context.visual.use_gravatar
1260 1260 _gravatar_url = request.call_context.visual.gravatar_url
1261 1261 else:
1262 1262 # doh, we need to re-import those to mock it later
1263 1263 from pylons import tmpl_context as c
1264 1264
1265 1265 _use_gravatar = c.visual.use_gravatar
1266 1266 _gravatar_url = c.visual.gravatar_url
1267 1267
1268 1268 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1269 1269
1270 1270 email_address = email_address or User.DEFAULT_USER_EMAIL
1271 1271 if isinstance(email_address, unicode):
1272 1272 # hashlib crashes on unicode items
1273 1273 email_address = safe_str(email_address)
1274 1274
1275 1275 # empty email or default user
1276 1276 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1277 1277 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1278 1278
1279 1279 if _use_gravatar:
1280 1280 # TODO: Disuse pyramid thread locals. Think about another solution to
1281 1281 # get the host and schema here.
1282 1282 request = get_current_request()
1283 1283 tmpl = safe_str(_gravatar_url)
1284 1284 tmpl = tmpl.replace('{email}', email_address)\
1285 1285 .replace('{md5email}', md5_safe(email_address.lower())) \
1286 1286 .replace('{netloc}', request.host)\
1287 1287 .replace('{scheme}', request.scheme)\
1288 1288 .replace('{size}', safe_str(size))
1289 1289 return tmpl
1290 1290 else:
1291 1291 return initials_gravatar(email_address, '', '', size=size)
1292 1292
1293 1293
1294 1294 class Page(_Page):
1295 1295 """
1296 1296 Custom pager to match rendering style with paginator
1297 1297 """
1298 1298
1299 1299 def _get_pos(self, cur_page, max_page, items):
1300 1300 edge = (items / 2) + 1
1301 1301 if (cur_page <= edge):
1302 1302 radius = max(items / 2, items - cur_page)
1303 1303 elif (max_page - cur_page) < edge:
1304 1304 radius = (items - 1) - (max_page - cur_page)
1305 1305 else:
1306 1306 radius = items / 2
1307 1307
1308 1308 left = max(1, (cur_page - (radius)))
1309 1309 right = min(max_page, cur_page + (radius))
1310 1310 return left, cur_page, right
1311 1311
1312 1312 def _range(self, regexp_match):
1313 1313 """
1314 1314 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1315 1315
1316 1316 Arguments:
1317 1317
1318 1318 regexp_match
1319 1319 A "re" (regular expressions) match object containing the
1320 1320 radius of linked pages around the current page in
1321 1321 regexp_match.group(1) as a string
1322 1322
1323 1323 This function is supposed to be called as a callable in
1324 1324 re.sub.
1325 1325
1326 1326 """
1327 1327 radius = int(regexp_match.group(1))
1328 1328
1329 1329 # Compute the first and last page number within the radius
1330 1330 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1331 1331 # -> leftmost_page = 5
1332 1332 # -> rightmost_page = 9
1333 1333 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1334 1334 self.last_page,
1335 1335 (radius * 2) + 1)
1336 1336 nav_items = []
1337 1337
1338 1338 # Create a link to the first page (unless we are on the first page
1339 1339 # or there would be no need to insert '..' spacers)
1340 1340 if self.page != self.first_page and self.first_page < leftmost_page:
1341 1341 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1342 1342
1343 1343 # Insert dots if there are pages between the first page
1344 1344 # and the currently displayed page range
1345 1345 if leftmost_page - self.first_page > 1:
1346 1346 # Wrap in a SPAN tag if nolink_attr is set
1347 1347 text = '..'
1348 1348 if self.dotdot_attr:
1349 1349 text = HTML.span(c=text, **self.dotdot_attr)
1350 1350 nav_items.append(text)
1351 1351
1352 1352 for thispage in xrange(leftmost_page, rightmost_page + 1):
1353 1353 # Hilight the current page number and do not use a link
1354 1354 if thispage == self.page:
1355 1355 text = '%s' % (thispage,)
1356 1356 # Wrap in a SPAN tag if nolink_attr is set
1357 1357 if self.curpage_attr:
1358 1358 text = HTML.span(c=text, **self.curpage_attr)
1359 1359 nav_items.append(text)
1360 1360 # Otherwise create just a link to that page
1361 1361 else:
1362 1362 text = '%s' % (thispage,)
1363 1363 nav_items.append(self._pagerlink(thispage, text))
1364 1364
1365 1365 # Insert dots if there are pages between the displayed
1366 1366 # page numbers and the end of the page range
1367 1367 if self.last_page - rightmost_page > 1:
1368 1368 text = '..'
1369 1369 # Wrap in a SPAN tag if nolink_attr is set
1370 1370 if self.dotdot_attr:
1371 1371 text = HTML.span(c=text, **self.dotdot_attr)
1372 1372 nav_items.append(text)
1373 1373
1374 1374 # Create a link to the very last page (unless we are on the last
1375 1375 # page or there would be no need to insert '..' spacers)
1376 1376 if self.page != self.last_page and rightmost_page < self.last_page:
1377 1377 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1378 1378
1379 1379 ## prerender links
1380 1380 #_page_link = url.current()
1381 1381 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1382 1382 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1383 1383 return self.separator.join(nav_items)
1384 1384
1385 1385 def pager(self, format='~2~', page_param='page', partial_param='partial',
1386 1386 show_if_single_page=False, separator=' ', onclick=None,
1387 1387 symbol_first='<<', symbol_last='>>',
1388 1388 symbol_previous='<', symbol_next='>',
1389 1389 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1390 1390 curpage_attr={'class': 'pager_curpage'},
1391 1391 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1392 1392
1393 1393 self.curpage_attr = curpage_attr
1394 1394 self.separator = separator
1395 1395 self.pager_kwargs = kwargs
1396 1396 self.page_param = page_param
1397 1397 self.partial_param = partial_param
1398 1398 self.onclick = onclick
1399 1399 self.link_attr = link_attr
1400 1400 self.dotdot_attr = dotdot_attr
1401 1401
1402 1402 # Don't show navigator if there is no more than one page
1403 1403 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1404 1404 return ''
1405 1405
1406 1406 from string import Template
1407 1407 # Replace ~...~ in token format by range of pages
1408 1408 result = re.sub(r'~(\d+)~', self._range, format)
1409 1409
1410 1410 # Interpolate '%' variables
1411 1411 result = Template(result).safe_substitute({
1412 1412 'first_page': self.first_page,
1413 1413 'last_page': self.last_page,
1414 1414 'page': self.page,
1415 1415 'page_count': self.page_count,
1416 1416 'items_per_page': self.items_per_page,
1417 1417 'first_item': self.first_item,
1418 1418 'last_item': self.last_item,
1419 1419 'item_count': self.item_count,
1420 1420 'link_first': self.page > self.first_page and \
1421 1421 self._pagerlink(self.first_page, symbol_first) or '',
1422 1422 'link_last': self.page < self.last_page and \
1423 1423 self._pagerlink(self.last_page, symbol_last) or '',
1424 1424 'link_previous': self.previous_page and \
1425 1425 self._pagerlink(self.previous_page, symbol_previous) \
1426 1426 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1427 1427 'link_next': self.next_page and \
1428 1428 self._pagerlink(self.next_page, symbol_next) \
1429 1429 or HTML.span(symbol_next, class_="pg-next disabled")
1430 1430 })
1431 1431
1432 1432 return literal(result)
1433 1433
1434 1434
1435 1435 #==============================================================================
1436 1436 # REPO PAGER, PAGER FOR REPOSITORY
1437 1437 #==============================================================================
1438 1438 class RepoPage(Page):
1439 1439
1440 1440 def __init__(self, collection, page=1, items_per_page=20,
1441 1441 item_count=None, url=None, **kwargs):
1442 1442
1443 1443 """Create a "RepoPage" instance. special pager for paging
1444 1444 repository
1445 1445 """
1446 1446 self._url_generator = url
1447 1447
1448 1448 # Safe the kwargs class-wide so they can be used in the pager() method
1449 1449 self.kwargs = kwargs
1450 1450
1451 1451 # Save a reference to the collection
1452 1452 self.original_collection = collection
1453 1453
1454 1454 self.collection = collection
1455 1455
1456 1456 # The self.page is the number of the current page.
1457 1457 # The first page has the number 1!
1458 1458 try:
1459 1459 self.page = int(page) # make it int() if we get it as a string
1460 1460 except (ValueError, TypeError):
1461 1461 self.page = 1
1462 1462
1463 1463 self.items_per_page = items_per_page
1464 1464
1465 1465 # Unless the user tells us how many items the collections has
1466 1466 # we calculate that ourselves.
1467 1467 if item_count is not None:
1468 1468 self.item_count = item_count
1469 1469 else:
1470 1470 self.item_count = len(self.collection)
1471 1471
1472 1472 # Compute the number of the first and last available page
1473 1473 if self.item_count > 0:
1474 1474 self.first_page = 1
1475 1475 self.page_count = int(math.ceil(float(self.item_count) /
1476 1476 self.items_per_page))
1477 1477 self.last_page = self.first_page + self.page_count - 1
1478 1478
1479 1479 # Make sure that the requested page number is the range of
1480 1480 # valid pages
1481 1481 if self.page > self.last_page:
1482 1482 self.page = self.last_page
1483 1483 elif self.page < self.first_page:
1484 1484 self.page = self.first_page
1485 1485
1486 1486 # Note: the number of items on this page can be less than
1487 1487 # items_per_page if the last page is not full
1488 1488 self.first_item = max(0, (self.item_count) - (self.page *
1489 1489 items_per_page))
1490 1490 self.last_item = ((self.item_count - 1) - items_per_page *
1491 1491 (self.page - 1))
1492 1492
1493 1493 self.items = list(self.collection[self.first_item:self.last_item + 1])
1494 1494
1495 1495 # Links to previous and next page
1496 1496 if self.page > self.first_page:
1497 1497 self.previous_page = self.page - 1
1498 1498 else:
1499 1499 self.previous_page = None
1500 1500
1501 1501 if self.page < self.last_page:
1502 1502 self.next_page = self.page + 1
1503 1503 else:
1504 1504 self.next_page = None
1505 1505
1506 1506 # No items available
1507 1507 else:
1508 1508 self.first_page = None
1509 1509 self.page_count = 0
1510 1510 self.last_page = None
1511 1511 self.first_item = None
1512 1512 self.last_item = None
1513 1513 self.previous_page = None
1514 1514 self.next_page = None
1515 1515 self.items = []
1516 1516
1517 1517 # This is a subclass of the 'list' type. Initialise the list now.
1518 1518 list.__init__(self, reversed(self.items))
1519 1519
1520 1520
1521 1521 def breadcrumb_repo_link(repo):
1522 1522 """
1523 1523 Makes a breadcrumbs path link to repo
1524 1524
1525 1525 ex::
1526 1526 group >> subgroup >> repo
1527 1527
1528 1528 :param repo: a Repository instance
1529 1529 """
1530 1530
1531 1531 path = [
1532 1532 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1533 1533 for group in repo.groups_with_parents
1534 1534 ] + [
1535 1535 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1536 1536 ]
1537 1537
1538 1538 return literal(' &raquo; '.join(path))
1539 1539
1540 1540
1541 1541 def format_byte_size_binary(file_size):
1542 1542 """
1543 1543 Formats file/folder sizes to standard.
1544 1544 """
1545 1545 if file_size is None:
1546 1546 file_size = 0
1547 1547
1548 1548 formatted_size = format_byte_size(file_size, binary=True)
1549 1549 return formatted_size
1550 1550
1551 1551
1552 1552 def urlify_text(text_, safe=True):
1553 1553 """
1554 1554 Extrac urls from text and make html links out of them
1555 1555
1556 1556 :param text_:
1557 1557 """
1558 1558
1559 1559 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1560 1560 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1561 1561
1562 1562 def url_func(match_obj):
1563 1563 url_full = match_obj.groups()[0]
1564 1564 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1565 1565 _newtext = url_pat.sub(url_func, text_)
1566 1566 if safe:
1567 1567 return literal(_newtext)
1568 1568 return _newtext
1569 1569
1570 1570
1571 1571 def urlify_commits(text_, repository):
1572 1572 """
1573 1573 Extract commit ids from text and make link from them
1574 1574
1575 1575 :param text_:
1576 1576 :param repository: repo name to build the URL with
1577 1577 """
1578 from pylons import url # doh, we need to re-import url to mock it later
1578
1579 1579 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1580 1580
1581 1581 def url_func(match_obj):
1582 1582 commit_id = match_obj.groups()[1]
1583 1583 pref = match_obj.groups()[0]
1584 1584 suf = match_obj.groups()[2]
1585 1585
1586 1586 tmpl = (
1587 1587 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1588 1588 '%(commit_id)s</a>%(suf)s'
1589 1589 )
1590 1590 return tmpl % {
1591 1591 'pref': pref,
1592 1592 'cls': 'revision-link',
1593 'url': url('changeset_home', repo_name=repository,
1594 revision=commit_id, qualified=True),
1593 'url': route_url('repo_commit', repo_name=repository,
1594 commit_id=commit_id),
1595 1595 'commit_id': commit_id,
1596 1596 'suf': suf
1597 1597 }
1598 1598
1599 1599 newtext = URL_PAT.sub(url_func, text_)
1600 1600
1601 1601 return newtext
1602 1602
1603 1603
1604 1604 def _process_url_func(match_obj, repo_name, uid, entry,
1605 1605 return_raw_data=False, link_format='html'):
1606 1606 pref = ''
1607 1607 if match_obj.group().startswith(' '):
1608 1608 pref = ' '
1609 1609
1610 1610 issue_id = ''.join(match_obj.groups())
1611 1611
1612 1612 if link_format == 'html':
1613 1613 tmpl = (
1614 1614 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1615 1615 '%(issue-prefix)s%(id-repr)s'
1616 1616 '</a>')
1617 1617 elif link_format == 'rst':
1618 1618 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1619 1619 elif link_format == 'markdown':
1620 1620 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1621 1621 else:
1622 1622 raise ValueError('Bad link_format:{}'.format(link_format))
1623 1623
1624 1624 (repo_name_cleaned,
1625 1625 parent_group_name) = RepoGroupModel().\
1626 1626 _get_group_name_and_parent(repo_name)
1627 1627
1628 1628 # variables replacement
1629 1629 named_vars = {
1630 1630 'id': issue_id,
1631 1631 'repo': repo_name,
1632 1632 'repo_name': repo_name_cleaned,
1633 1633 'group_name': parent_group_name
1634 1634 }
1635 1635 # named regex variables
1636 1636 named_vars.update(match_obj.groupdict())
1637 1637 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1638 1638
1639 1639 data = {
1640 1640 'pref': pref,
1641 1641 'cls': 'issue-tracker-link',
1642 1642 'url': _url,
1643 1643 'id-repr': issue_id,
1644 1644 'issue-prefix': entry['pref'],
1645 1645 'serv': entry['url'],
1646 1646 }
1647 1647 if return_raw_data:
1648 1648 return {
1649 1649 'id': issue_id,
1650 1650 'url': _url
1651 1651 }
1652 1652 return tmpl % data
1653 1653
1654 1654
1655 1655 def process_patterns(text_string, repo_name, link_format='html'):
1656 1656 allowed_formats = ['html', 'rst', 'markdown']
1657 1657 if link_format not in allowed_formats:
1658 1658 raise ValueError('Link format can be only one of:{} got {}'.format(
1659 1659 allowed_formats, link_format))
1660 1660
1661 1661 repo = None
1662 1662 if repo_name:
1663 1663 # Retrieving repo_name to avoid invalid repo_name to explode on
1664 1664 # IssueTrackerSettingsModel but still passing invalid name further down
1665 1665 repo = Repository.get_by_repo_name(repo_name, cache=True)
1666 1666
1667 1667 settings_model = IssueTrackerSettingsModel(repo=repo)
1668 1668 active_entries = settings_model.get_settings(cache=True)
1669 1669
1670 1670 issues_data = []
1671 1671 newtext = text_string
1672 1672
1673 1673 for uid, entry in active_entries.items():
1674 1674 log.debug('found issue tracker entry with uid %s' % (uid,))
1675 1675
1676 1676 if not (entry['pat'] and entry['url']):
1677 1677 log.debug('skipping due to missing data')
1678 1678 continue
1679 1679
1680 1680 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1681 1681 % (uid, entry['pat'], entry['url'], entry['pref']))
1682 1682
1683 1683 try:
1684 1684 pattern = re.compile(r'%s' % entry['pat'])
1685 1685 except re.error:
1686 1686 log.exception(
1687 1687 'issue tracker pattern: `%s` failed to compile',
1688 1688 entry['pat'])
1689 1689 continue
1690 1690
1691 1691 data_func = partial(
1692 1692 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1693 1693 return_raw_data=True)
1694 1694
1695 1695 for match_obj in pattern.finditer(text_string):
1696 1696 issues_data.append(data_func(match_obj))
1697 1697
1698 1698 url_func = partial(
1699 1699 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1700 1700 link_format=link_format)
1701 1701
1702 1702 newtext = pattern.sub(url_func, newtext)
1703 1703 log.debug('processed prefix:uid `%s`' % (uid,))
1704 1704
1705 1705 return newtext, issues_data
1706 1706
1707 1707
1708 1708 def urlify_commit_message(commit_text, repository=None):
1709 1709 """
1710 1710 Parses given text message and makes proper links.
1711 1711 issues are linked to given issue-server, and rest is a commit link
1712 1712
1713 1713 :param commit_text:
1714 1714 :param repository:
1715 1715 """
1716 1716 from pylons import url # doh, we need to re-import url to mock it later
1717 1717
1718 1718 def escaper(string):
1719 1719 return string.replace('<', '&lt;').replace('>', '&gt;')
1720 1720
1721 1721 newtext = escaper(commit_text)
1722 1722
1723 1723 # extract http/https links and make them real urls
1724 1724 newtext = urlify_text(newtext, safe=False)
1725 1725
1726 1726 # urlify commits - extract commit ids and make link out of them, if we have
1727 1727 # the scope of repository present.
1728 1728 if repository:
1729 1729 newtext = urlify_commits(newtext, repository)
1730 1730
1731 1731 # process issue tracker patterns
1732 1732 newtext, issues = process_patterns(newtext, repository or '')
1733 1733
1734 1734 return literal(newtext)
1735 1735
1736 1736
1737 1737 def render_binary(repo_name, file_obj):
1738 1738 """
1739 1739 Choose how to render a binary file
1740 1740 """
1741 1741 filename = file_obj.name
1742 1742
1743 1743 # images
1744 1744 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1745 1745 if fnmatch.fnmatch(filename, pat=ext):
1746 1746 alt = filename
1747 1747 src = route_path(
1748 1748 'repo_file_raw', repo_name=repo_name,
1749 1749 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1750 1750 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1751 1751
1752 1752
1753 1753 def renderer_from_filename(filename, exclude=None):
1754 1754 """
1755 1755 choose a renderer based on filename, this works only for text based files
1756 1756 """
1757 1757
1758 1758 # ipython
1759 1759 for ext in ['*.ipynb']:
1760 1760 if fnmatch.fnmatch(filename, pat=ext):
1761 1761 return 'jupyter'
1762 1762
1763 1763 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1764 1764 if is_markup:
1765 1765 return is_markup
1766 1766 return None
1767 1767
1768 1768
1769 1769 def render(source, renderer='rst', mentions=False, relative_url=None,
1770 1770 repo_name=None):
1771 1771
1772 1772 def maybe_convert_relative_links(html_source):
1773 1773 if relative_url:
1774 1774 return relative_links(html_source, relative_url)
1775 1775 return html_source
1776 1776
1777 1777 if renderer == 'rst':
1778 1778 if repo_name:
1779 1779 # process patterns on comments if we pass in repo name
1780 1780 source, issues = process_patterns(
1781 1781 source, repo_name, link_format='rst')
1782 1782
1783 1783 return literal(
1784 1784 '<div class="rst-block">%s</div>' %
1785 1785 maybe_convert_relative_links(
1786 1786 MarkupRenderer.rst(source, mentions=mentions)))
1787 1787 elif renderer == 'markdown':
1788 1788 if repo_name:
1789 1789 # process patterns on comments if we pass in repo name
1790 1790 source, issues = process_patterns(
1791 1791 source, repo_name, link_format='markdown')
1792 1792
1793 1793 return literal(
1794 1794 '<div class="markdown-block">%s</div>' %
1795 1795 maybe_convert_relative_links(
1796 1796 MarkupRenderer.markdown(source, flavored=True,
1797 1797 mentions=mentions)))
1798 1798 elif renderer == 'jupyter':
1799 1799 return literal(
1800 1800 '<div class="ipynb">%s</div>' %
1801 1801 maybe_convert_relative_links(
1802 1802 MarkupRenderer.jupyter(source)))
1803 1803
1804 1804 # None means just show the file-source
1805 1805 return None
1806 1806
1807 1807
1808 1808 def commit_status(repo, commit_id):
1809 1809 return ChangesetStatusModel().get_status(repo, commit_id)
1810 1810
1811 1811
1812 1812 def commit_status_lbl(commit_status):
1813 1813 return dict(ChangesetStatus.STATUSES).get(commit_status)
1814 1814
1815 1815
1816 1816 def commit_time(repo_name, commit_id):
1817 1817 repo = Repository.get_by_repo_name(repo_name)
1818 1818 commit = repo.get_commit(commit_id=commit_id)
1819 1819 return commit.date
1820 1820
1821 1821
1822 1822 def get_permission_name(key):
1823 1823 return dict(Permission.PERMS).get(key)
1824 1824
1825 1825
1826 1826 def journal_filter_help(request):
1827 1827 _ = request.translate
1828 1828
1829 1829 return _(
1830 1830 'Example filter terms:\n' +
1831 1831 ' repository:vcs\n' +
1832 1832 ' username:marcin\n' +
1833 1833 ' username:(NOT marcin)\n' +
1834 1834 ' action:*push*\n' +
1835 1835 ' ip:127.0.0.1\n' +
1836 1836 ' date:20120101\n' +
1837 1837 ' date:[20120101100000 TO 20120102]\n' +
1838 1838 '\n' +
1839 1839 'Generate wildcards using \'*\' character:\n' +
1840 1840 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1841 1841 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1842 1842 '\n' +
1843 1843 'Optional AND / OR operators in queries\n' +
1844 1844 ' "repository:vcs OR repository:test"\n' +
1845 1845 ' "username:test AND repository:test*"\n'
1846 1846 )
1847 1847
1848 1848
1849 1849 def search_filter_help(searcher, request):
1850 1850 _ = request.translate
1851 1851
1852 1852 terms = ''
1853 1853 return _(
1854 1854 'Example filter terms for `{searcher}` search:\n' +
1855 1855 '{terms}\n' +
1856 1856 'Generate wildcards using \'*\' character:\n' +
1857 1857 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1858 1858 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1859 1859 '\n' +
1860 1860 'Optional AND / OR operators in queries\n' +
1861 1861 ' "repo_name:vcs OR repo_name:test"\n' +
1862 1862 ' "owner:test AND repo_name:test*"\n' +
1863 1863 'More: {search_doc}'
1864 1864 ).format(searcher=searcher.name,
1865 1865 terms=terms, search_doc=searcher.query_lang_doc)
1866 1866
1867 1867
1868 1868 def not_mapped_error(repo_name):
1869 1869 from rhodecode.translation import _
1870 1870 flash(_('%s repository is not mapped to db perhaps'
1871 1871 ' it was created or renamed from the filesystem'
1872 1872 ' please run the application again'
1873 1873 ' in order to rescan repositories') % repo_name, category='error')
1874 1874
1875 1875
1876 1876 def ip_range(ip_addr):
1877 1877 from rhodecode.model.db import UserIpMap
1878 1878 s, e = UserIpMap._get_ip_range(ip_addr)
1879 1879 return '%s - %s' % (s, e)
1880 1880
1881 1881
1882 1882 def form(url, method='post', needs_csrf_token=True, **attrs):
1883 1883 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1884 1884 if method.lower() != 'get' and needs_csrf_token:
1885 1885 raise Exception(
1886 1886 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1887 1887 'CSRF token. If the endpoint does not require such token you can ' +
1888 1888 'explicitly set the parameter needs_csrf_token to false.')
1889 1889
1890 1890 return wh_form(url, method=method, **attrs)
1891 1891
1892 1892
1893 1893 def secure_form(url, method="POST", multipart=False, **attrs):
1894 1894 """Start a form tag that points the action to an url. This
1895 1895 form tag will also include the hidden field containing
1896 1896 the auth token.
1897 1897
1898 1898 The url options should be given either as a string, or as a
1899 1899 ``url()`` function. The method for the form defaults to POST.
1900 1900
1901 1901 Options:
1902 1902
1903 1903 ``multipart``
1904 1904 If set to True, the enctype is set to "multipart/form-data".
1905 1905 ``method``
1906 1906 The method to use when submitting the form, usually either
1907 1907 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1908 1908 hidden input with name _method is added to simulate the verb
1909 1909 over POST.
1910 1910
1911 1911 """
1912 1912 from webhelpers.pylonslib.secure_form import insecure_form
1913 1913 form = insecure_form(url, method, multipart, **attrs)
1914 1914
1915 1915 session = None
1916 1916 # TODO(marcink): after pyramid migration require request variable ALWAYS
1917 1917 if 'request' in attrs:
1918 1918 session = attrs['request'].session
1919 1919
1920 1920 token = literal(
1921 1921 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1922 1922 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1923 1923
1924 1924 return literal("%s\n%s" % (form, token))
1925 1925
1926 1926
1927 1927 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1928 1928 select_html = select(name, selected, options, **attrs)
1929 1929 select2 = """
1930 1930 <script>
1931 1931 $(document).ready(function() {
1932 1932 $('#%s').select2({
1933 1933 containerCssClass: 'drop-menu',
1934 1934 dropdownCssClass: 'drop-menu-dropdown',
1935 1935 dropdownAutoWidth: true%s
1936 1936 });
1937 1937 });
1938 1938 </script>
1939 1939 """
1940 1940 filter_option = """,
1941 1941 minimumResultsForSearch: -1
1942 1942 """
1943 1943 input_id = attrs.get('id') or name
1944 1944 filter_enabled = "" if enable_filter else filter_option
1945 1945 select_script = literal(select2 % (input_id, filter_enabled))
1946 1946
1947 1947 return literal(select_html+select_script)
1948 1948
1949 1949
1950 1950 def get_visual_attr(tmpl_context_var, attr_name):
1951 1951 """
1952 1952 A safe way to get a variable from visual variable of template context
1953 1953
1954 1954 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1955 1955 :param attr_name: name of the attribute we fetch from the c.visual
1956 1956 """
1957 1957 visual = getattr(tmpl_context_var, 'visual', None)
1958 1958 if not visual:
1959 1959 return
1960 1960 else:
1961 1961 return getattr(visual, attr_name, None)
1962 1962
1963 1963
1964 1964 def get_last_path_part(file_node):
1965 1965 if not file_node.path:
1966 1966 return u''
1967 1967
1968 1968 path = safe_unicode(file_node.path.split('/')[-1])
1969 1969 return u'../' + path
1970 1970
1971 1971
1972 1972 def route_url(*args, **kwargs):
1973 1973 """
1974 1974 Wrapper around pyramids `route_url` (fully qualified url) function.
1975 1975 It is used to generate URLs from within pylons views or templates.
1976 1976 This will be removed when pyramid migration if finished.
1977 1977 """
1978 1978 req = get_current_request()
1979 1979 return req.route_url(*args, **kwargs)
1980 1980
1981 1981
1982 1982 def route_path(*args, **kwargs):
1983 1983 """
1984 1984 Wrapper around pyramids `route_path` function. It is used to generate
1985 1985 URLs from within pylons views or templates. This will be removed when
1986 1986 pyramid migration if finished.
1987 1987 """
1988 1988 req = get_current_request()
1989 1989 return req.route_path(*args, **kwargs)
1990 1990
1991 1991
1992 1992 def route_path_or_none(*args, **kwargs):
1993 1993 try:
1994 1994 return route_path(*args, **kwargs)
1995 1995 except KeyError:
1996 1996 return None
1997 1997
1998 1998
1999 1999 def static_url(*args, **kwds):
2000 2000 """
2001 2001 Wrapper around pyramids `route_path` function. It is used to generate
2002 2002 URLs from within pylons views or templates. This will be removed when
2003 2003 pyramid migration if finished.
2004 2004 """
2005 2005 req = get_current_request()
2006 2006 return req.static_url(*args, **kwds)
2007 2007
2008 2008
2009 2009 def resource_path(*args, **kwds):
2010 2010 """
2011 2011 Wrapper around pyramids `route_path` function. It is used to generate
2012 2012 URLs from within pylons views or templates. This will be removed when
2013 2013 pyramid migration if finished.
2014 2014 """
2015 2015 req = get_current_request()
2016 2016 return req.resource_path(*args, **kwds)
2017 2017
2018 2018
2019 2019 def api_call_example(method, args):
2020 2020 """
2021 2021 Generates an API call example via CURL
2022 2022 """
2023 2023 args_json = json.dumps(OrderedDict([
2024 2024 ('id', 1),
2025 2025 ('auth_token', 'SECRET'),
2026 2026 ('method', method),
2027 2027 ('args', args)
2028 2028 ]))
2029 2029 return literal(
2030 2030 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2031 2031 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2032 2032 "and needs to be of `api calls` role."
2033 2033 .format(
2034 2034 api_url=route_url('apiv2'),
2035 2035 token_url=route_url('my_account_auth_tokens'),
2036 2036 data=args_json))
2037 2037
2038 2038
2039 2039 def notification_description(notification, request):
2040 2040 """
2041 2041 Generate notification human readable description based on notification type
2042 2042 """
2043 2043 from rhodecode.model.notification import NotificationModel
2044 2044 return NotificationModel().make_description(
2045 2045 notification, translate=request.translate)
@@ -1,214 +1,216 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('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
19 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
20 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
21 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
22 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
23 18 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']);
24 19 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
25 20 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
26 21 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
27 22 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
28 23 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
29 24 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
30 25 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
31 26 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
32 27 pyroutes.register('favicon', '/favicon.ico', []);
33 28 pyroutes.register('robots', '/robots.txt', []);
34 29 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
35 30 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
36 31 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
37 32 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
38 33 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
39 34 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
40 35 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
41 36 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
42 37 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
43 38 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
44 39 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
45 40 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
46 41 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
47 42 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
48 43 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
49 44 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
50 45 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
51 46 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
52 47 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
53 48 pyroutes.register('admin_home', '/_admin', []);
54 49 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
55 50 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
56 51 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
57 52 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
58 53 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
59 54 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
60 55 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
61 56 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
62 57 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
63 58 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
64 59 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
65 60 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
66 61 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
67 62 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
68 63 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
69 64 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
70 65 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
71 66 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
72 67 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
73 68 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
74 69 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
75 70 pyroutes.register('users', '/_admin/users', []);
76 71 pyroutes.register('users_data', '/_admin/users_data', []);
77 72 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
78 73 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
79 74 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
80 75 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
81 76 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
82 77 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
83 78 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
84 79 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
85 80 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
86 81 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
87 82 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
88 83 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
89 84 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
90 85 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
91 86 pyroutes.register('channelstream_proxy', '/_channelstream', []);
92 87 pyroutes.register('login', '/_admin/login', []);
93 88 pyroutes.register('logout', '/_admin/logout', []);
94 89 pyroutes.register('register', '/_admin/register', []);
95 90 pyroutes.register('reset_password', '/_admin/password_reset', []);
96 91 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
97 92 pyroutes.register('home', '/', []);
98 93 pyroutes.register('user_autocomplete_data', '/_users', []);
99 94 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
100 95 pyroutes.register('repo_list_data', '/_repos', []);
101 96 pyroutes.register('goto_switcher_data', '/_goto_data', []);
102 97 pyroutes.register('journal', '/_admin/journal', []);
103 98 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
104 99 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
105 100 pyroutes.register('journal_public', '/_admin/public_journal', []);
106 101 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
107 102 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
108 103 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
109 104 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
110 105 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
111 106 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
112 107 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
113 108 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
109 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
110 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
111 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
112 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
113 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
114 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
115 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
116 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
117 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
118 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
114 119 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
115 120 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
116 121 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
117 122 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
118 123 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
119 124 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
120 125 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
121 126 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 127 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
123 128 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
124 129 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
125 130 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 131 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
127 132 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
128 133 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
129 134 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
130 135 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
131 136 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
132 137 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
133 138 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
134 139 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
135 140 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
136 141 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
137 142 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
138 143 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
139 144 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
140 145 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
141 146 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
142 147 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
143 148 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
144 149 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
145 150 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
146 151 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
147 152 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
148 153 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
149 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
150 pyroutes.register('changeset_children', '/%(repo_name)s/changeset_children/%(revision)s', ['repo_name', 'revision']);
151 pyroutes.register('changeset_parents', '/%(repo_name)s/changeset_parents/%(revision)s', ['repo_name', 'revision']);
152 154 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
153 155 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
154 156 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
155 157 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
156 158 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
157 159 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
158 160 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
159 161 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
160 162 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
161 163 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
162 164 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
163 165 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
164 166 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
165 167 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
166 168 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
167 169 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
168 170 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
169 171 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
170 172 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
171 173 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
172 174 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
173 175 pyroutes.register('search', '/_admin/search', []);
174 176 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
175 177 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
176 178 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
177 179 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
178 180 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
179 181 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
180 182 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
181 183 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
182 184 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
183 185 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
184 186 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
185 187 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
186 188 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
187 189 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
188 190 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
189 191 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
190 192 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
191 193 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
192 194 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
193 195 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
194 196 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
195 197 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
196 198 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
197 199 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
198 200 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
199 201 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
200 202 pyroutes.register('gists_show', '/_admin/gists', []);
201 203 pyroutes.register('gists_new', '/_admin/gists/new', []);
202 204 pyroutes.register('gists_create', '/_admin/gists/create', []);
203 205 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
204 206 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
205 207 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
206 208 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
207 209 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
208 210 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
209 211 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
210 212 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
211 213 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
212 214 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
213 215 pyroutes.register('apiv2', '/_admin/api', []);
214 216 }
@@ -1,830 +1,831 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 46 (function(mod) {
47 47
48 48 if (typeof exports == "object" && typeof module == "object") {
49 49 // CommonJS
50 50 module.exports = mod();
51 51 }
52 52 else {
53 53 // Plain browser env
54 54 (this || window).CommentForm = mod();
55 55 }
56 56
57 57 })(function() {
58 58 "use strict";
59 59
60 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 61 if (!(this instanceof CommentForm)) {
62 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 63 }
64 64
65 65 // bind the element instance to our Form
66 66 $(formElement).get(0).CommentForm = this;
67 67
68 68 this.withLineNo = function(selector) {
69 69 var lineNo = this.lineNo;
70 70 if (lineNo === undefined) {
71 71 return selector
72 72 } else {
73 73 return selector + '_' + lineNo;
74 74 }
75 75 };
76 76
77 77 this.commitId = commitId;
78 78 this.pullRequestId = pullRequestId;
79 79 this.lineNo = lineNo;
80 80 this.initAutocompleteActions = initAutocompleteActions;
81 81
82 82 this.previewButton = this.withLineNo('#preview-btn');
83 83 this.previewContainer = this.withLineNo('#preview-container');
84 84
85 85 this.previewBoxSelector = this.withLineNo('#preview-box');
86 86
87 87 this.editButton = this.withLineNo('#edit-btn');
88 88 this.editContainer = this.withLineNo('#edit-container');
89 89 this.cancelButton = this.withLineNo('#cancel-btn');
90 90 this.commentType = this.withLineNo('#comment_type');
91 91
92 92 this.resolvesId = null;
93 93 this.resolvesActionId = null;
94 94
95 95 this.closesPr = '#close_pull_request';
96 96
97 97 this.cmBox = this.withLineNo('#text');
98 98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
99 99
100 100 this.statusChange = this.withLineNo('#change_status');
101 101
102 102 this.submitForm = formElement;
103 103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
104 104 this.submitButtonText = this.submitButton.val();
105 105
106 this.previewUrl = pyroutes.url('changeset_comment_preview',
107 {'repo_name': templateContext.repo_name});
106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
107 {'repo_name': templateContext.repo_name,
108 'commit_id': templateContext.commit_data.commit_id});
108 109
109 110 if (resolvesCommentId){
110 111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
111 112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
112 113 $(this.commentType).prop('disabled', true);
113 114 $(this.commentType).addClass('disabled');
114 115
115 116 // disable select
116 117 setTimeout(function() {
117 118 $(self.statusChange).select2('readonly', true);
118 119 }, 10);
119 120
120 121 var resolvedInfo = (
121 122 '<li class="resolve-action">' +
122 123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
123 124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
124 125 '</li>'
125 126 ).format(resolvesCommentId, _gettext('resolve comment'));
126 127 $(resolvedInfo).insertAfter($(this.commentType).parent());
127 128 }
128 129
129 130 // based on commitId, or pullRequestId decide where do we submit
130 131 // out data
131 132 if (this.commitId){
132 this.submitUrl = pyroutes.url('changeset_comment',
133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
133 134 {'repo_name': templateContext.repo_name,
134 'revision': this.commitId});
135 this.selfUrl = pyroutes.url('changeset_home',
135 'commit_id': this.commitId});
136 this.selfUrl = pyroutes.url('repo_commit',
136 137 {'repo_name': templateContext.repo_name,
137 'revision': this.commitId});
138 'commit_id': this.commitId});
138 139
139 140 } else if (this.pullRequestId) {
140 141 this.submitUrl = pyroutes.url('pullrequest_comment',
141 142 {'repo_name': templateContext.repo_name,
142 143 'pull_request_id': this.pullRequestId});
143 144 this.selfUrl = pyroutes.url('pullrequest_show',
144 145 {'repo_name': templateContext.repo_name,
145 146 'pull_request_id': this.pullRequestId});
146 147
147 148 } else {
148 149 throw new Error(
149 150 'CommentForm requires pullRequestId, or commitId to be specified.')
150 151 }
151 152
152 153 // FUNCTIONS and helpers
153 154 var self = this;
154 155
155 156 this.isInline = function(){
156 157 return this.lineNo && this.lineNo != 'general';
157 158 };
158 159
159 160 this.getCmInstance = function(){
160 161 return this.cm
161 162 };
162 163
163 164 this.setPlaceholder = function(placeholder) {
164 165 var cm = this.getCmInstance();
165 166 if (cm){
166 167 cm.setOption('placeholder', placeholder);
167 168 }
168 169 };
169 170
170 171 this.getCommentStatus = function() {
171 172 return $(this.submitForm).find(this.statusChange).val();
172 173 };
173 174 this.getCommentType = function() {
174 175 return $(this.submitForm).find(this.commentType).val();
175 176 };
176 177
177 178 this.getResolvesId = function() {
178 179 return $(this.submitForm).find(this.resolvesId).val() || null;
179 180 };
180 181
181 182 this.getClosePr = function() {
182 183 return $(this.submitForm).find(this.closesPr).val() || null;
183 184 };
184 185
185 186 this.markCommentResolved = function(resolvedCommentId){
186 187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
187 188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
188 189 };
189 190
190 191 this.isAllowedToSubmit = function() {
191 192 return !$(this.submitButton).prop('disabled');
192 193 };
193 194
194 195 this.initStatusChangeSelector = function(){
195 196 var formatChangeStatus = function(state, escapeMarkup) {
196 197 var originalOption = state.element;
197 198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
198 199 '<span>' + escapeMarkup(state.text) + '</span>';
199 200 };
200 201 var formatResult = function(result, container, query, escapeMarkup) {
201 202 return formatChangeStatus(result, escapeMarkup);
202 203 };
203 204
204 205 var formatSelection = function(data, container, escapeMarkup) {
205 206 return formatChangeStatus(data, escapeMarkup);
206 207 };
207 208
208 209 $(this.submitForm).find(this.statusChange).select2({
209 210 placeholder: _gettext('Status Review'),
210 211 formatResult: formatResult,
211 212 formatSelection: formatSelection,
212 213 containerCssClass: "drop-menu status_box_menu",
213 214 dropdownCssClass: "drop-menu-dropdown",
214 215 dropdownAutoWidth: true,
215 216 minimumResultsForSearch: -1
216 217 });
217 218 $(this.submitForm).find(this.statusChange).on('change', function() {
218 219 var status = self.getCommentStatus();
219 220
220 221 if (status && !self.isInline()) {
221 222 $(self.submitButton).prop('disabled', false);
222 223 }
223 224
224 225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
225 226 self.setPlaceholder(placeholderText)
226 227 })
227 228 };
228 229
229 230 // reset the comment form into it's original state
230 231 this.resetCommentFormState = function(content) {
231 232 content = content || '';
232 233
233 234 $(this.editContainer).show();
234 235 $(this.editButton).parent().addClass('active');
235 236
236 237 $(this.previewContainer).hide();
237 238 $(this.previewButton).parent().removeClass('active');
238 239
239 240 this.setActionButtonsDisabled(true);
240 241 self.cm.setValue(content);
241 242 self.cm.setOption("readOnly", false);
242 243
243 244 if (this.resolvesId) {
244 245 // destroy the resolve action
245 246 $(this.resolvesId).parent().remove();
246 247 }
247 248 // reset closingPR flag
248 249 $('.close-pr-input').remove();
249 250
250 251 $(this.statusChange).select2('readonly', false);
251 252 };
252 253
253 254 this.globalSubmitSuccessCallback = function(){
254 255 // default behaviour is to call GLOBAL hook, if it's registered.
255 256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
256 257 commentFormGlobalSubmitSuccessCallback()
257 258 }
258 259 };
259 260
260 261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
261 262 failHandler = failHandler || function() {};
262 263 var postData = toQueryString(postData);
263 264 var request = $.ajax({
264 265 url: url,
265 266 type: 'POST',
266 267 data: postData,
267 268 headers: {'X-PARTIAL-XHR': true}
268 269 })
269 270 .done(function(data) {
270 271 successHandler(data);
271 272 })
272 273 .fail(function(data, textStatus, errorThrown){
273 274 alert(
274 275 "Error while submitting comment.\n" +
275 276 "Error code {0} ({1}).".format(data.status, data.statusText));
276 277 failHandler()
277 278 });
278 279 return request;
279 280 };
280 281
281 282 // overwrite a submitHandler, we need to do it for inline comments
282 283 this.setHandleFormSubmit = function(callback) {
283 284 this.handleFormSubmit = callback;
284 285 };
285 286
286 287 // overwrite a submitSuccessHandler
287 288 this.setGlobalSubmitSuccessCallback = function(callback) {
288 289 this.globalSubmitSuccessCallback = callback;
289 290 };
290 291
291 292 // default handler for for submit for main comments
292 293 this.handleFormSubmit = function() {
293 294 var text = self.cm.getValue();
294 295 var status = self.getCommentStatus();
295 296 var commentType = self.getCommentType();
296 297 var resolvesCommentId = self.getResolvesId();
297 298 var closePullRequest = self.getClosePr();
298 299
299 300 if (text === "" && !status) {
300 301 return;
301 302 }
302 303
303 304 var excludeCancelBtn = false;
304 305 var submitEvent = true;
305 306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
306 307 self.cm.setOption("readOnly", true);
307 308
308 309 var postData = {
309 310 'text': text,
310 311 'changeset_status': status,
311 312 'comment_type': commentType,
312 313 'csrf_token': CSRF_TOKEN
313 314 };
314 315
315 316 if (resolvesCommentId) {
316 317 postData['resolves_comment_id'] = resolvesCommentId;
317 318 }
318 319
319 320 if (closePullRequest) {
320 321 postData['close_pull_request'] = true;
321 322 }
322 323
323 324 var submitSuccessCallback = function(o) {
324 325 // reload page if we change status for single commit.
325 326 if (status && self.commitId) {
326 327 location.reload(true);
327 328 } else {
328 329 $('#injected_page_comments').append(o.rendered_text);
329 330 self.resetCommentFormState();
330 331 timeagoActivate();
331 332
332 333 // mark visually which comment was resolved
333 334 if (resolvesCommentId) {
334 335 self.markCommentResolved(resolvesCommentId);
335 336 }
336 337 }
337 338
338 339 // run global callback on submit
339 340 self.globalSubmitSuccessCallback();
340 341
341 342 };
342 343 var submitFailCallback = function(){
343 344 self.resetCommentFormState(text);
344 345 };
345 346 self.submitAjaxPOST(
346 347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
347 348 };
348 349
349 350 this.previewSuccessCallback = function(o) {
350 351 $(self.previewBoxSelector).html(o);
351 352 $(self.previewBoxSelector).removeClass('unloaded');
352 353
353 354 // swap buttons, making preview active
354 355 $(self.previewButton).parent().addClass('active');
355 356 $(self.editButton).parent().removeClass('active');
356 357
357 358 // unlock buttons
358 359 self.setActionButtonsDisabled(false);
359 360 };
360 361
361 362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
362 363 excludeCancelBtn = excludeCancelBtn || false;
363 364 submitEvent = submitEvent || false;
364 365
365 366 $(this.editButton).prop('disabled', state);
366 367 $(this.previewButton).prop('disabled', state);
367 368
368 369 if (!excludeCancelBtn) {
369 370 $(this.cancelButton).prop('disabled', state);
370 371 }
371 372
372 373 var submitState = state;
373 374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
374 375 // if the value of commit review status is set, we allow
375 376 // submit button, but only on Main form, isInline means inline
376 377 submitState = false
377 378 }
378 379
379 380 $(this.submitButton).prop('disabled', submitState);
380 381 if (submitEvent) {
381 382 $(this.submitButton).val(_gettext('Submitting...'));
382 383 } else {
383 384 $(this.submitButton).val(this.submitButtonText);
384 385 }
385 386
386 387 };
387 388
388 389 // lock preview/edit/submit buttons on load, but exclude cancel button
389 390 var excludeCancelBtn = true;
390 391 this.setActionButtonsDisabled(true, excludeCancelBtn);
391 392
392 393 // anonymous users don't have access to initialized CM instance
393 394 if (this.cm !== undefined){
394 395 this.cm.on('change', function(cMirror) {
395 396 if (cMirror.getValue() === "") {
396 397 self.setActionButtonsDisabled(true, excludeCancelBtn)
397 398 } else {
398 399 self.setActionButtonsDisabled(false, excludeCancelBtn)
399 400 }
400 401 });
401 402 }
402 403
403 404 $(this.editButton).on('click', function(e) {
404 405 e.preventDefault();
405 406
406 407 $(self.previewButton).parent().removeClass('active');
407 408 $(self.previewContainer).hide();
408 409
409 410 $(self.editButton).parent().addClass('active');
410 411 $(self.editContainer).show();
411 412
412 413 });
413 414
414 415 $(this.previewButton).on('click', function(e) {
415 416 e.preventDefault();
416 417 var text = self.cm.getValue();
417 418
418 419 if (text === "") {
419 420 return;
420 421 }
421 422
422 423 var postData = {
423 424 'text': text,
424 425 'renderer': templateContext.visual.default_renderer,
425 426 'csrf_token': CSRF_TOKEN
426 427 };
427 428
428 429 // lock ALL buttons on preview
429 430 self.setActionButtonsDisabled(true);
430 431
431 432 $(self.previewBoxSelector).addClass('unloaded');
432 433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
433 434
434 435 $(self.editContainer).hide();
435 436 $(self.previewContainer).show();
436 437
437 438 // by default we reset state of comment preserving the text
438 439 var previewFailCallback = function(){
439 440 self.resetCommentFormState(text)
440 441 };
441 442 self.submitAjaxPOST(
442 443 self.previewUrl, postData, self.previewSuccessCallback,
443 444 previewFailCallback);
444 445
445 446 $(self.previewButton).parent().addClass('active');
446 447 $(self.editButton).parent().removeClass('active');
447 448 });
448 449
449 450 $(this.submitForm).submit(function(e) {
450 451 e.preventDefault();
451 452 var allowedToSubmit = self.isAllowedToSubmit();
452 453 if (!allowedToSubmit){
453 454 return false;
454 455 }
455 456 self.handleFormSubmit();
456 457 });
457 458
458 459 }
459 460
460 461 return CommentForm;
461 462 });
462 463
463 464 /* comments controller */
464 465 var CommentsController = function() {
465 466 var mainComment = '#text';
466 467 var self = this;
467 468
468 469 this.cancelComment = function(node) {
469 470 var $node = $(node);
470 471 var $td = $node.closest('td');
471 472 $node.closest('.comment-inline-form').remove();
472 473 return false;
473 474 };
474 475
475 476 this.getLineNumber = function(node) {
476 477 var $node = $(node);
477 478 return $node.closest('td').attr('data-line-number');
478 479 };
479 480
480 481 this.scrollToComment = function(node, offset, outdated) {
481 482 if (offset === undefined) {
482 483 offset = 0;
483 484 }
484 485 var outdated = outdated || false;
485 486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
486 487
487 488 if (!node) {
488 489 node = $('.comment-selected');
489 490 if (!node.length) {
490 491 node = $('comment-current')
491 492 }
492 493 }
493 494 $wrapper = $(node).closest('div.comment');
494 495 $comment = $(node).closest(klass);
495 496 $comments = $(klass);
496 497
497 498 // show hidden comment when referenced.
498 499 if (!$wrapper.is(':visible')){
499 500 $wrapper.show();
500 501 }
501 502
502 503 $('.comment-selected').removeClass('comment-selected');
503 504
504 505 var nextIdx = $(klass).index($comment) + offset;
505 506 if (nextIdx >= $comments.length) {
506 507 nextIdx = 0;
507 508 }
508 509 var $next = $(klass).eq(nextIdx);
509 510
510 511 var $cb = $next.closest('.cb');
511 512 $cb.removeClass('cb-collapsed');
512 513
513 514 var $filediffCollapseState = $cb.closest('.filediff').prev();
514 515 $filediffCollapseState.prop('checked', false);
515 516 $next.addClass('comment-selected');
516 517 scrollToElement($next);
517 518 return false;
518 519 };
519 520
520 521 this.nextComment = function(node) {
521 522 return self.scrollToComment(node, 1);
522 523 };
523 524
524 525 this.prevComment = function(node) {
525 526 return self.scrollToComment(node, -1);
526 527 };
527 528
528 529 this.nextOutdatedComment = function(node) {
529 530 return self.scrollToComment(node, 1, true);
530 531 };
531 532
532 533 this.prevOutdatedComment = function(node) {
533 534 return self.scrollToComment(node, -1, true);
534 535 };
535 536
536 537 this.deleteComment = function(node) {
537 538 if (!confirm(_gettext('Delete this comment?'))) {
538 539 return false;
539 540 }
540 541 var $node = $(node);
541 542 var $td = $node.closest('td');
542 543 var $comment = $node.closest('.comment');
543 544 var comment_id = $comment.attr('data-comment-id');
544 545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
545 546 var postData = {
546 547 '_method': 'delete',
547 548 'csrf_token': CSRF_TOKEN
548 549 };
549 550
550 551 $comment.addClass('comment-deleting');
551 552 $comment.hide('fast');
552 553
553 554 var success = function(response) {
554 555 $comment.remove();
555 556 return false;
556 557 };
557 558 var failure = function(data, textStatus, xhr) {
558 559 alert("error processing request: " + textStatus);
559 560 $comment.show('fast');
560 561 $comment.removeClass('comment-deleting');
561 562 return false;
562 563 };
563 564 ajaxPOST(url, postData, success, failure);
564 565 };
565 566
566 567 this.toggleWideMode = function (node) {
567 568 if ($('#content').hasClass('wrapper')) {
568 569 $('#content').removeClass("wrapper");
569 570 $('#content').addClass("wide-mode-wrapper");
570 571 $(node).addClass('btn-success');
571 572 } else {
572 573 $('#content').removeClass("wide-mode-wrapper");
573 574 $('#content').addClass("wrapper");
574 575 $(node).removeClass('btn-success');
575 576 }
576 577 return false;
577 578 };
578 579
579 580 this.toggleComments = function(node, show) {
580 581 var $filediff = $(node).closest('.filediff');
581 582 if (show === true) {
582 583 $filediff.removeClass('hide-comments');
583 584 } else if (show === false) {
584 585 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
585 586 $filediff.addClass('hide-comments');
586 587 } else {
587 588 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
588 589 $filediff.toggleClass('hide-comments');
589 590 }
590 591 return false;
591 592 };
592 593
593 594 this.toggleLineComments = function(node) {
594 595 self.toggleComments(node, true);
595 596 var $node = $(node);
596 597 $node.closest('tr').toggleClass('hide-line-comments');
597 598 };
598 599
599 600 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
600 601 var pullRequestId = templateContext.pull_request_data.pull_request_id;
601 602 var commitId = templateContext.commit_data.commit_id;
602 603
603 604 var commentForm = new CommentForm(
604 605 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
605 606 var cm = commentForm.getCmInstance();
606 607
607 608 if (resolvesCommentId){
608 609 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
609 610 }
610 611
611 612 setTimeout(function() {
612 613 // callbacks
613 614 if (cm !== undefined) {
614 615 commentForm.setPlaceholder(placeholderText);
615 616 if (commentForm.isInline()) {
616 617 cm.focus();
617 618 cm.refresh();
618 619 }
619 620 }
620 621 }, 10);
621 622
622 623 // trigger scrolldown to the resolve comment, since it might be away
623 624 // from the clicked
624 625 if (resolvesCommentId){
625 626 var actionNode = $(commentForm.resolvesActionId).offset();
626 627
627 628 setTimeout(function() {
628 629 if (actionNode) {
629 630 $('body, html').animate({scrollTop: actionNode.top}, 10);
630 631 }
631 632 }, 100);
632 633 }
633 634
634 635 return commentForm;
635 636 };
636 637
637 638 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
638 639
639 640 var tmpl = $('#cb-comment-general-form-template').html();
640 641 tmpl = tmpl.format(null, 'general');
641 642 var $form = $(tmpl);
642 643
643 644 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
644 645 var curForm = $formPlaceholder.find('form');
645 646 if (curForm){
646 647 curForm.remove();
647 648 }
648 649 $formPlaceholder.append($form);
649 650
650 651 var _form = $($form[0]);
651 652 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
652 653 var commentForm = this.createCommentForm(
653 654 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
654 655 commentForm.initStatusChangeSelector();
655 656
656 657 return commentForm;
657 658 };
658 659
659 660 this.createComment = function(node, resolutionComment) {
660 661 var resolvesCommentId = resolutionComment || null;
661 662 var $node = $(node);
662 663 var $td = $node.closest('td');
663 664 var $form = $td.find('.comment-inline-form');
664 665
665 666 if (!$form.length) {
666 667
667 668 var $filediff = $node.closest('.filediff');
668 669 $filediff.removeClass('hide-comments');
669 670 var f_path = $filediff.attr('data-f-path');
670 671 var lineno = self.getLineNumber(node);
671 672 // create a new HTML from template
672 673 var tmpl = $('#cb-comment-inline-form-template').html();
673 674 tmpl = tmpl.format(f_path, lineno);
674 675 $form = $(tmpl);
675 676
676 677 var $comments = $td.find('.inline-comments');
677 678 if (!$comments.length) {
678 679 $comments = $(
679 680 $('#cb-comments-inline-container-template').html());
680 681 $td.append($comments);
681 682 }
682 683
683 684 $td.find('.cb-comment-add-button').before($form);
684 685
685 686 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
686 687 var _form = $($form[0]).find('form');
687 688 var autocompleteActions = ['as_note', 'as_todo'];
688 689 var commentForm = this.createCommentForm(
689 690 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
690 691
691 692 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
692 693 form: _form,
693 694 parent: $td[0],
694 695 lineno: lineno,
695 696 f_path: f_path}
696 697 );
697 698
698 699 // set a CUSTOM submit handler for inline comments.
699 700 commentForm.setHandleFormSubmit(function(o) {
700 701 var text = commentForm.cm.getValue();
701 702 var commentType = commentForm.getCommentType();
702 703 var resolvesCommentId = commentForm.getResolvesId();
703 704
704 705 if (text === "") {
705 706 return;
706 707 }
707 708
708 709 if (lineno === undefined) {
709 710 alert('missing line !');
710 711 return;
711 712 }
712 713 if (f_path === undefined) {
713 714 alert('missing file path !');
714 715 return;
715 716 }
716 717
717 718 var excludeCancelBtn = false;
718 719 var submitEvent = true;
719 720 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
720 721 commentForm.cm.setOption("readOnly", true);
721 722 var postData = {
722 723 'text': text,
723 724 'f_path': f_path,
724 725 'line': lineno,
725 726 'comment_type': commentType,
726 727 'csrf_token': CSRF_TOKEN
727 728 };
728 729 if (resolvesCommentId){
729 730 postData['resolves_comment_id'] = resolvesCommentId;
730 731 }
731 732
732 733 var submitSuccessCallback = function(json_data) {
733 734 $form.remove();
734 735 try {
735 736 var html = json_data.rendered_text;
736 737 var lineno = json_data.line_no;
737 738 var target_id = json_data.target_id;
738 739
739 740 $comments.find('.cb-comment-add-button').before(html);
740 741
741 742 //mark visually which comment was resolved
742 743 if (resolvesCommentId) {
743 744 commentForm.markCommentResolved(resolvesCommentId);
744 745 }
745 746
746 747 // run global callback on submit
747 748 commentForm.globalSubmitSuccessCallback();
748 749
749 750 } catch (e) {
750 751 console.error(e);
751 752 }
752 753
753 754 // re trigger the linkification of next/prev navigation
754 755 linkifyComments($('.inline-comment-injected'));
755 756 timeagoActivate();
756 757 commentForm.setActionButtonsDisabled(false);
757 758
758 759 };
759 760 var submitFailCallback = function(){
760 761 commentForm.resetCommentFormState(text)
761 762 };
762 763 commentForm.submitAjaxPOST(
763 764 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
764 765 });
765 766 }
766 767
767 768 $form.addClass('comment-inline-form-open');
768 769 };
769 770
770 771 this.createResolutionComment = function(commentId){
771 772 // hide the trigger text
772 773 $('#resolve-comment-{0}'.format(commentId)).hide();
773 774
774 775 var comment = $('#comment-'+commentId);
775 776 var commentData = comment.data();
776 777 if (commentData.commentInline) {
777 778 this.createComment(comment, commentId)
778 779 } else {
779 780 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
780 781 }
781 782
782 783 return false;
783 784 };
784 785
785 786 this.submitResolution = function(commentId){
786 787 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
787 788 var commentForm = form.get(0).CommentForm;
788 789
789 790 var cm = commentForm.getCmInstance();
790 791 var renderer = templateContext.visual.default_renderer;
791 792 if (renderer == 'rst'){
792 793 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
793 794 } else if (renderer == 'markdown') {
794 795 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
795 796 } else {
796 797 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
797 798 }
798 799
799 800 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
800 801 form.submit();
801 802 return false;
802 803 };
803 804
804 805 this.renderInlineComments = function(file_comments) {
805 806 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
806 807
807 808 for (var i = 0; i < file_comments.length; i++) {
808 809 var box = file_comments[i];
809 810
810 811 var target_id = $(box).attr('target_id');
811 812
812 813 // actually comments with line numbers
813 814 var comments = box.children;
814 815
815 816 for (var j = 0; j < comments.length; j++) {
816 817 var data = {
817 818 'rendered_text': comments[j].outerHTML,
818 819 'line_no': $(comments[j]).attr('line'),
819 820 'target_id': target_id
820 821 };
821 822 }
822 823 }
823 824
824 825 // since order of injection is random, we're now re-iterating
825 826 // from correct order and filling in links
826 827 linkifyComments($('.inline-comment-injected'));
827 828 firefoxAnchorFix();
828 829 };
829 830
830 831 };
@@ -1,210 +1,210 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Owner'), lambda:base.gravatar_with_user(c.repo_info.user.email), '', ''),
6 6 (_('Created on'), h.format_date(c.repo_info.created_on), '', ''),
7 7 (_('Updated on'), h.format_date(c.repo_info.updated_on), '', ''),
8 (_('Cached Commit id'), lambda: h.link_to(c.repo_info.changeset_cache.get('short_id'), h.url('changeset_home',repo_name=c.repo_name,revision=c.repo_info.changeset_cache.get('raw_id'))), '', ''),
8 (_('Cached Commit id'), lambda: h.link_to(c.repo_info.changeset_cache.get('short_id'), h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.repo_info.changeset_cache.get('raw_id'))), '', ''),
9 9 ]
10 10 %>
11 11
12 12 <div class="panel panel-default">
13 13 <div class="panel-heading" id="advanced-info" >
14 14 <h3 class="panel-title">${_('Repository: %s') % c.repo_info.repo_name} <a class="permalink" href="#advanced-info"></a></h3>
15 15 </div>
16 16 <div class="panel-body">
17 17 ${base.dt_info_panel(elems)}
18 18 </div>
19 19 </div>
20 20
21 21
22 22 <div class="panel panel-default">
23 23 <div class="panel-heading" id="advanced-fork">
24 24 <h3 class="panel-title">${_('Fork Reference')} <a class="permalink" href="#advanced-fork"></a></h3>
25 25 </div>
26 26 <div class="panel-body">
27 27 ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.repo_info.repo_name), method='POST', request=request)}
28 28
29 29 % if c.repo_info.fork:
30 30 <div class="panel-body-title-text">${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.repo_info.fork.repo_name, h.route_path('repo_summary', repo_name=c.repo_info.fork.repo_name))})}
31 31 | <button class="btn btn-link btn-danger" type="submit">Remove fork reference</button></div>
32 32 % endif
33 33
34 34 <div class="field">
35 35 ${h.hidden('id_fork_of')}
36 36 ${h.submit('set_as_fork_%s' % c.repo_info.repo_name,_('Set'),class_="btn btn-small",)}
37 37 </div>
38 38 <div class="field">
39 39 <span class="help-block">${_('Manually set this repository as a fork of another from the list')}</span>
40 40 </div>
41 41 ${h.end_form()}
42 42 </div>
43 43 </div>
44 44
45 45
46 46 <div class="panel panel-default">
47 47 <div class="panel-heading" id="advanced-journal">
48 48 <h3 class="panel-title">${_('Public Journal Visibility')} <a class="permalink" href="#advanced-journal"></a></h3>
49 49 </div>
50 50 <div class="panel-body">
51 51 ${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.repo_info.repo_name), method='POST', request=request)}
52 52 <div class="field">
53 53 %if c.in_public_journal:
54 54 <button class="btn btn-small" type="submit">
55 55 ${_('Remove from Public Journal')}
56 56 </button>
57 57 %else:
58 58 <button class="btn btn-small" type="submit">
59 59 ${_('Add to Public Journal')}
60 60 </button>
61 61 %endif
62 62 </div>
63 63 <div class="field" >
64 64 <span class="help-block">${_('All actions made on this repository will be visible to everyone following the public journal.')}</span>
65 65 </div>
66 66 ${h.end_form()}
67 67 </div>
68 68 </div>
69 69
70 70
71 71 <div class="panel panel-default">
72 72 <div class="panel-heading" id="advanced-locking">
73 73 <h3 class="panel-title">${_('Locking state')} <a class="permalink" href="#advanced-locking"></a></h3>
74 74 </div>
75 75 <div class="panel-body">
76 76 ${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.repo_info.repo_name), method='POST', request=request)}
77 77
78 78 %if c.repo_info.locked[0]:
79 79 <div class="panel-body-title-text">${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.repo_info.locked[0]),
80 80 h.format_date(h. time_to_datetime(c.repo_info.locked[1])), c.repo_info.locked[2])}</div>
81 81 %else:
82 82 <div class="panel-body-title-text">${_('This Repository is not currently locked.')}</div>
83 83 %endif
84 84
85 85 <div class="field" >
86 86 %if c.repo_info.locked[0]:
87 87 ${h.hidden('set_unlock', '1')}
88 88 <button class="btn btn-small" type="submit"
89 89 onclick="return confirm('${_('Confirm to unlock repository.')}');">
90 90 <i class="icon-unlock"></i>
91 91 ${_('Unlock repository')}
92 92 </button>
93 93 %else:
94 94 ${h.hidden('set_lock', '1')}
95 95 <button class="btn btn-small" type="submit"
96 96 onclick="return confirm('${_('Confirm to lock repository.')}');">
97 97 <i class="icon-lock"></i>
98 98 ${_('Lock Repository')}
99 99 </button>
100 100 %endif
101 101 </div>
102 102 <div class="field" >
103 103 <span class="help-block">
104 104 ${_('Force repository locking. This only works when anonymous access is disabled. Pulling from the repository locks the repository to that user until the same user pushes to that repository again.')}
105 105 </span>
106 106 </div>
107 107 ${h.end_form()}
108 108 </div>
109 109 </div>
110 110
111 111 <div class="panel panel-danger">
112 112 <div class="panel-heading" id="advanced-delete">
113 113 <h3 class="panel-title">${_('Delete repository')} <a class="permalink" href="#advanced-delete"></a></h3>
114 114 </div>
115 115 <div class="panel-body">
116 116 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), method='POST', request=request)}
117 117 <table class="display">
118 118 <tr>
119 119 <td>
120 120 ${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.repo_info.forks.count()) % c.repo_info.forks.count()}
121 121 </td>
122 122 <td>
123 123 %if c.repo_info.forks.count():
124 124 <input type="radio" name="forks" value="detach_forks" checked="checked"/> <label for="forks">${_('Detach forks')}</label>
125 125 %endif
126 126 </td>
127 127 <td>
128 128 %if c.repo_info.forks.count():
129 129 <input type="radio" name="forks" value="delete_forks"/> <label for="forks">${_('Delete forks')}</label>
130 130 %endif
131 131 </td>
132 132 </tr>
133 133 </table>
134 134 <div style="margin: 0 0 20px 0" class="fake-space"></div>
135 135
136 136 <div class="field">
137 137 <button class="btn btn-small btn-danger" type="submit"
138 138 onclick="return confirm('${_('Confirm to delete this repository: %s') % c.repo_name}');">
139 139 <i class="icon-remove-sign"></i>
140 140 ${_('Delete This Repository')}
141 141 </button>
142 142 </div>
143 143 <div class="field">
144 144 <span class="help-block">
145 145 ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')}
146 146 </span>
147 147 </div>
148 148
149 149 ${h.end_form()}
150 150 </div>
151 151 </div>
152 152
153 153
154 154 <script>
155 155
156 156 var currentRepoId = ${c.repo_info.repo_id};
157 157
158 158 var repoTypeFilter = function(data) {
159 159 var results = [];
160 160
161 161 if (!data.results[0]) {
162 162 return data
163 163 }
164 164
165 165 $.each(data.results[0].children, function() {
166 166 // filter out the SAME repo, it cannot be used as fork of itself
167 167 if (this.obj.repo_id != currentRepoId) {
168 168 this.id = this.obj.repo_id;
169 169 results.push(this)
170 170 }
171 171 });
172 172 data.results[0].children = results;
173 173 return data;
174 174 };
175 175
176 176 $("#id_fork_of").select2({
177 177 cachedDataSource: {},
178 178 minimumInputLength: 2,
179 179 placeholder: "${_('Change repository') if c.repo_info.fork else _('Pick repository')}",
180 180 dropdownAutoWidth: true,
181 181 containerCssClass: "drop-menu",
182 182 dropdownCssClass: "drop-menu-dropdown",
183 183 formatResult: formatResult,
184 184 query: $.debounce(250, function(query){
185 185 self = this;
186 186 var cacheKey = query.term;
187 187 var cachedData = self.cachedDataSource[cacheKey];
188 188
189 189 if (cachedData) {
190 190 query.callback({results: cachedData.results});
191 191 } else {
192 192 $.ajax({
193 193 url: pyroutes.url('repo_list_data'),
194 194 data: {'query': query.term, repo_type: '${c.repo_info.repo_type}'},
195 195 dataType: 'json',
196 196 type: 'GET',
197 197 success: function(data) {
198 198 data = repoTypeFilter(data);
199 199 self.cachedDataSource[cacheKey] = data;
200 200 query.callback({results: data.results});
201 201 },
202 202 error: function(data, textStatus, errorThrown) {
203 203 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
204 204 }
205 205 })
206 206 }
207 207 })
208 208 });
209 209 </script>
210 210
@@ -1,299 +1,299 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 %if c.changelog_for_path:
17 17 /${c.changelog_for_path}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='changelog')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30
31 31 <div class="box">
32 32 <div class="title">
33 33 ${self.repo_page_title(c.rhodecode_db_repo)}
34 34 <ul class="links">
35 35 <li>
36 36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
37 37 %if c.rhodecode_db_repo.fork:
38 38 <span>
39 39 <a id="compare_fork_button"
40 40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
41 41 class="btn btn-small"
42 42 href="${h.url('compare_url',
43 43 repo_name=c.rhodecode_db_repo.fork.repo_name,
44 44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
45 45 source_ref=c.rhodecode_db_repo.landing_rev[1],
46 46 target_repo=c.repo_name,
47 47 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
48 48 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
49 49 merge=1)}"
50 50 >
51 51 <i class="icon-loop"></i>
52 52 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
53 53 </a>
54 54 </span>
55 55 %endif
56 56
57 57 ## pr open link
58 58 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
59 59 <span>
60 60 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
61 61 ${_('Open new pull request')}
62 62 </a>
63 63 </span>
64 64 %endif
65 65
66 66 ## clear selection
67 67 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
68 68 ${_('Clear selection')}
69 69 </div>
70 70
71 71 </li>
72 72 </ul>
73 73 </div>
74 74
75 75 % if c.pagination:
76 76 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
77 77
78 78 <div class="graph-header">
79 79 <div id="filter_changelog">
80 80 ${h.hidden('branch_filter')}
81 81 %if c.selected_name:
82 82 <div class="btn btn-default" id="clear_filter" >
83 83 ${_('Clear filter')}
84 84 </div>
85 85 %endif
86 86 </div>
87 87 ${self.breadcrumbs('breadcrumbs_light')}
88 88 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
89 89 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
90 90 </div>
91 91 </div>
92 92
93 93 <div id="graph">
94 94 <div class="graph-col-wrapper">
95 95 <div id="graph_nodes">
96 96 <div id="graph_canvas"></div>
97 97 </div>
98 98 <div id="graph_content" class="main-content graph_full_width">
99 99
100 100 <div class="table">
101 101 <table id="changesets" class="rctable">
102 102 <tr>
103 103 ## checkbox
104 104 <th></th>
105 105 <th colspan="2"></th>
106 106
107 107 <th>${_('Commit')}</th>
108 108 ## commit message expand arrow
109 109 <th></th>
110 110 <th>${_('Commit Message')}</th>
111 111
112 112 <th>${_('Age')}</th>
113 113 <th>${_('Author')}</th>
114 114
115 115 <th>${_('Refs')}</th>
116 116 </tr>
117 117
118 118 <tbody class="commits-range">
119 119 <%include file='changelog_elements.mako'/>
120 120 </tbody>
121 121 </table>
122 122 </div>
123 123 </div>
124 124 <div class="pagination-wh pagination-left">
125 125 ${c.pagination.pager('$link_previous ~2~ $link_next')}
126 126 </div>
127 127 </div>
128 128
129 129 <script type="text/javascript">
130 130 var cache = {};
131 131 $(function(){
132 132
133 133 // Create links to commit ranges when range checkboxes are selected
134 134 var $commitCheckboxes = $('.commit-range');
135 135 // cache elements
136 136 var $commitRangeContainer = $('#rev_range_container');
137 137 var $commitRangeClear = $('#rev_range_clear');
138 138
139 139 var checkboxRangeSelector = function(e){
140 140 var selectedCheckboxes = [];
141 141 for (pos in $commitCheckboxes){
142 142 if($commitCheckboxes[pos].checked){
143 143 selectedCheckboxes.push($commitCheckboxes[pos]);
144 144 }
145 145 }
146 146 var open_new_pull_request = $('#open_new_pull_request');
147 147 if(open_new_pull_request){
148 148 var selected_changes = selectedCheckboxes.length;
149 149 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
150 150 open_new_pull_request.hide();
151 151 } else {
152 152 if (selected_changes == 1) {
153 153 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
154 154 } else if (selected_changes == 0) {
155 155 open_new_pull_request.html(_gettext('Open new pull request'));
156 156 }
157 157 open_new_pull_request.show();
158 158 }
159 159 }
160 160
161 161 if (selectedCheckboxes.length>0){
162 162 var revEnd = selectedCheckboxes[0].name;
163 163 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
164 var url = pyroutes.url('changeset_home',
164 var url = pyroutes.url('repo_commit',
165 165 {'repo_name': '${c.repo_name}',
166 'revision': revStart+'...'+revEnd});
166 'commit_id': revStart+'...'+revEnd});
167 167
168 168 var link = (revStart == revEnd)
169 169 ? _gettext('Show selected commit __S')
170 170 : _gettext('Show selected commits __S ... __E');
171 171
172 172 link = link.replace('__S', revStart.substr(0,6));
173 173 link = link.replace('__E', revEnd.substr(0,6));
174 174
175 175 $commitRangeContainer
176 176 .attr('href',url)
177 177 .html(link)
178 178 .show();
179 179
180 180 $commitRangeClear.show();
181 181 var _url = pyroutes.url('pullrequest_home',
182 182 {'repo_name': '${c.repo_name}',
183 183 'commit': revEnd});
184 184 open_new_pull_request.attr('href', _url);
185 185 $('#compare_fork_button').hide();
186 186 } else {
187 187 $commitRangeContainer.hide();
188 188 $commitRangeClear.hide();
189 189
190 190 %if c.branch_name:
191 191 var _url = pyroutes.url('pullrequest_home',
192 192 {'repo_name': '${c.repo_name}',
193 193 'branch':'${c.branch_name}'});
194 194 open_new_pull_request.attr('href', _url);
195 195 %else:
196 196 var _url = pyroutes.url('pullrequest_home',
197 197 {'repo_name': '${c.repo_name}'});
198 198 open_new_pull_request.attr('href', _url);
199 199 %endif
200 200 $('#compare_fork_button').show();
201 201 }
202 202 };
203 203
204 204 $commitCheckboxes.on('click', checkboxRangeSelector);
205 205
206 206 $commitRangeClear.on('click',function(e) {
207 207 $commitCheckboxes.attr('checked', false);
208 208 checkboxRangeSelector();
209 209 e.preventDefault();
210 210 });
211 211
212 212 // make sure the buttons are consistent when navigate back and forth
213 213 checkboxRangeSelector();
214 214
215 215 var msgs = $('.message');
216 216 // get first element height
217 217 var el = $('#graph_content .container')[0];
218 218 var row_h = el.clientHeight;
219 219 for (var i=0; i < msgs.length; i++) {
220 220 var m = msgs[i];
221 221
222 222 var h = m.clientHeight;
223 223 var pad = $(m).css('padding');
224 224 if (h > row_h) {
225 225 var offset = row_h - (h+12);
226 226 $(m.nextElementSibling).css('display','block');
227 227 $(m.nextElementSibling).css('margin-top',offset+'px');
228 228 }
229 229 }
230 230
231 231 $("#clear_filter").on("click", function() {
232 232 var filter = {'repo_name': '${c.repo_name}'};
233 233 window.location = pyroutes.url('repo_changelog', filter);
234 234 });
235 235
236 236 $("#branch_filter").select2({
237 237 'dropdownAutoWidth': true,
238 238 'width': 'resolve',
239 239 'placeholder': "${c.selected_name or _('Filter changelog')}",
240 240 containerCssClass: "drop-menu",
241 241 dropdownCssClass: "drop-menu-dropdown",
242 242 query: function(query){
243 243 var key = 'cache';
244 244 var cached = cache[key] ;
245 245 if(cached) {
246 246 var data = {results: []};
247 247 //filter results
248 248 $.each(cached.results, function(){
249 249 var section = this.text;
250 250 var children = [];
251 251 $.each(this.children, function(){
252 252 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
253 253 children.push({'id': this.id, 'text': this.text, 'type': this.type})
254 254 }
255 255 });
256 256 data.results.push({'text': section, 'children': children});
257 257 query.callback({results: data.results});
258 258 });
259 259 }else{
260 260 $.ajax({
261 261 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
262 262 data: {},
263 263 dataType: 'json',
264 264 type: 'GET',
265 265 success: function(data) {
266 266 cache[key] = data;
267 267 query.callback({results: data.results});
268 268 }
269 269 })
270 270 }
271 271 }
272 272 });
273 273 $('#branch_filter').on('change', function(e){
274 274 var data = $('#branch_filter').select2('data');
275 275 var selected = data.text;
276 276 var filter = {'repo_name': '${c.repo_name}'};
277 277 if(data.type == 'branch' || data.type == 'branch_closed'){
278 278 filter["branch"] = selected;
279 279 }
280 280 else if (data.type == 'book'){
281 281 filter["bookmark"] = selected;
282 282 }
283 283 window.location = pyroutes.url('repo_changelog', filter);
284 284 });
285 285
286 286 commitsController = new CommitsController();
287 287 % if not c.changelog_for_path:
288 288 commitsController.reloadGraph();
289 289 % endif
290 290
291 291 });
292 292
293 293 </script>
294 294 </div>
295 295 % else:
296 296 ${_('There are no changes yet')}
297 297 % endif
298 298 </div>
299 299 </%def>
@@ -1,144 +1,144 b''
1 1 ## small box that displays changed/added/removed details fetched by AJAX
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4
5 5 % if c.prev_page:
6 6 <tr>
7 7 <td colspan="9" class="load-more-commits">
8 8 <a class="prev-commits" href="#loadPrevCommits" onclick="commitsController.loadPrev(this, ${c.prev_page}, '${c.branch_name}');return false">
9 9 ${_('load previous')}
10 10 </a>
11 11 </td>
12 12 </tr>
13 13 % endif
14 14
15 15 % for cnt,commit in enumerate(c.pagination):
16 16 <tr id="sha_${commit.raw_id}" class="changelogRow container ${'tablerow%s' % (cnt%2)}">
17 17
18 18 <td class="td-checkbox">
19 19 ${h.checkbox(commit.raw_id,class_="commit-range")}
20 20 </td>
21 21 <td class="td-status">
22 22
23 23 %if c.statuses.get(commit.raw_id):
24 24 <div class="changeset-status-ico">
25 25 %if c.statuses.get(commit.raw_id)[2]:
26 26 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.route_path('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
27 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
27 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
28 28 </a>
29 29 %else:
30 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(commit.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
31 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
30 <a class="tooltip" title="${_('Commit status: {}').format(h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]))}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id,_anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
31 <div class="${'flag_status {}'.format(c.statuses.get(commit.raw_id)[0])}"></div>
32 32 </a>
33 33 %endif
34 34 </div>
35 35 %else:
36 36 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
37 37 %endif
38 38 </td>
39 39 <td class="td-comments comments-col">
40 40 %if c.comments.get(commit.raw_id):
41 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
41 <a title="${_('Commit has comments')}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id,_anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
42 42 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
43 43 </a>
44 44 %endif
45 45 </td>
46 46 <td class="td-hash">
47 47 <code>
48 48
49 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">
49 <a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
50 50 <span class="${'commit_hash obsolete' if getattr(commit, 'obsolete', None) else 'commit_hash'}">${h.show_id(commit)}</span>
51 51 </a>
52 52 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
53 53 % if hasattr(commit, 'phase'):
54 54 % if commit.phase != 'public':
55 55 <span class="tag phase-${commit.phase} tooltip" title="${_('Commit phase')}">${commit.phase}</span>
56 56 % endif
57 57 % endif
58 58
59 59 ## obsolete commits
60 60 % if hasattr(commit, 'obsolete'):
61 61 % if commit.obsolete:
62 62 <span class="tag obsolete-${commit.obsolete} tooltip" title="${_('Evolve State')}">${_('obsolete')}</span>
63 63 % endif
64 64 % endif
65 65
66 66 ## hidden commits
67 67 % if hasattr(commit, 'hidden'):
68 68 % if commit.hidden:
69 69 <span class="tag obsolete-${commit.hidden} tooltip" title="${_('Evolve State')}">${_('hidden')}</span>
70 70 % endif
71 71 % endif
72 72
73 73 </code>
74 74 </td>
75 75 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
76 76 <div class="show_more_col">
77 77 <i class="show_more"></i>&nbsp;
78 78 </div>
79 79 </td>
80 80 <td class="td-description mid">
81 81 <div class="log-container truncate-wrap">
82 82 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
83 83 </div>
84 84 </td>
85 85
86 86 <td class="td-time">
87 87 ${h.age_component(commit.date)}
88 88 </td>
89 89 <td class="td-user">
90 90 ${base.gravatar_with_user(commit.author)}
91 91 </td>
92 92
93 93 <td class="td-tags tags-col">
94 94 <div id="t-${commit.raw_id}">
95 95
96 96 ## merge
97 97 %if commit.merge:
98 98 <span class="tag mergetag">
99 99 <i class="icon-merge"></i>${_('merge')}
100 100 </span>
101 101 %endif
102 102
103 103 ## branch
104 104 %if commit.branch:
105 105 <span class="tag branchtag" title="${h.tooltip(_('Branch %s') % commit.branch)}">
106 106 <a href="${h.route_path('repo_changelog',repo_name=c.repo_name,_query=dict(branch=commit.branch))}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
107 107 </span>
108 108 %endif
109 109
110 110 ## bookmarks
111 111 %if h.is_hg(c.rhodecode_repo):
112 112 %for book in commit.bookmarks:
113 113 <span class="tag booktag" title="${h.tooltip(_('Bookmark %s') % book)}">
114 114 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=book))}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
115 115 </span>
116 116 %endfor
117 117 %endif
118 118
119 119 ## tags
120 120 %for tag in commit.tags:
121 121 <span class="tag tagtag" title="${h.tooltip(_('Tag %s') % tag)}">
122 122 <a href="${h.route_path('repo_files:default_path',repo_name=c.repo_name,commit_id=commit.raw_id, _query=dict(at=tag))}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
123 123 </span>
124 124 %endfor
125 125
126 126 </div>
127 127 </td>
128 128 </tr>
129 129 % endfor
130 130
131 131 % if c.next_page:
132 132 <tr>
133 133 <td colspan="9" class="load-more-commits">
134 134 <a class="next-commits" href="#loadNextCommits" onclick="commitsController.loadNext(this, ${c.next_page}, '${c.branch_name}');return false">
135 135 ${_('load next')}
136 136 </a>
137 137 </td>
138 138 </tr>
139 139 % endif
140 140 <tr class="chunk-graph-data" style="display:none"
141 141 data-graph='${c.graph_data|n}'
142 142 data-node='${c.prev_page}:${c.next_page}'
143 143 data-commits='${c.graph_commits|n}'>
144 144 </tr> No newline at end of file
@@ -1,39 +1,39 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <div class="table">
3 3
4 4 <table class="table rctable file_history">
5 5 %for cnt,cs in enumerate(c.pagination):
6 6 <tr id="chg_${cnt+1}" class="${'tablerow%s' % (cnt%2)}">
7 7 <td class="td-user">
8 8 ${base.gravatar_with_user(cs.author, 16)}
9 9 </td>
10 10 <td class="td-time">
11 11 <div class="date">
12 12 ${h.age_component(cs.date)}
13 13 </div>
14 14 </td>
15 15 <td class="td-message">
16 16 <div class="log-container">
17 17 <div class="message_history" title="${h.tooltip(cs.message)}">
18 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">
18 <a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=cs.raw_id)}">
19 19 ${h.shorter(cs.message, 75)}
20 20 </a>
21 21 </div>
22 22 </div>
23 23 </td>
24 24 <td class="td-hash">
25 25 <code>
26 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">
26 <a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=cs.raw_id)}">
27 27 <span>${h.show_id(cs)}</span>
28 28 </a>
29 29 </code>
30 30 </td>
31 31 <td class="td-actions">
32 32 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=cs.raw_id,f_path=c.changelog_for_path)}">
33 33 ${_('Show File')}
34 34 </a>
35 35 </td>
36 36 </tr>
37 37 %endfor
38 38 </table>
39 39 </div>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now