##// END OF EJS Templates
merge 1.3 into stable
marcink -
r2031:82a88013 merge default
parent child Browse files
Show More

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

1 NO CONTENT: new file 100644
@@ -0,0 +1,34 b''
1 # celeryd - run the celeryd daemon as an upstart job for rhodecode
2 # Change variables/paths as necessary and place file /etc/init/celeryd.conf
3 # start/stop/restart as normal upstart job (ie: $ start celeryd)
4
5 description "Celery for RhodeCode Mercurial Server"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
7
8 start on starting rhodecode
9 stop on stopped rhodecode
10
11 respawn
12
13 umask 0022
14
15 env PIDFILE=/tmp/celeryd.pid
16 env APPINI=/var/hg/rhodecode/production.ini
17 env HOME=/var/hg
18 env USER=hg
19 # To use group (if different from user), you must edit sudoers file and change
20 # root's entry from (ALL) to (ALL:ALL)
21 # env GROUP=hg
22
23 script
24 COMMAND="/var/hg/.virtualenvs/rhodecode/bin/paster celeryd $APPINI --pidfile=$PIDFILE"
25 if [ -z "$GROUP" ]; then
26 exec sudo -u $USER $COMMAND
27 else
28 exec sudo -u $USER -g $GROUP $COMMAND
29 fi
30 end script
31
32 post-stop script
33 rm -f $PIDFILE
34 end script
@@ -0,0 +1,26 b''
1 # rhodecode - run the rhodecode daemon as an upstart job
2 # Change variables/paths as necessary and place file /etc/init/rhodecode.conf
3 # start/stop/restart as normal upstart job (ie: $ start rhodecode)
4
5 description "RhodeCode Mercurial Server"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
7
8 start on (local-filesystems and runlevel [2345])
9 stop on runlevel [!2345]
10
11 respawn
12
13 umask 0022
14
15 env PIDFILE=/var/hg/rhodecode/rhodecode.pid
16 env LOGFILE=/var/hg/rhodecode/log/rhodecode.log
17 env APPINI=/var/hg/rhodecode/production.ini
18 env HOME=/var/hg
19 env USER=hg
20 env GROUP=hg
21
22 exec /var/hg/.virtualenvs/rhodecode/bin/paster serve --user=$USER --group=$GROUP --pid-file=$PIDFILE --log-file=$LOGFILE $APPINI
23
24 post-stop script
25 rm -f $PIDFILE
26 end script
@@ -0,0 +1,140 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.admin.notifications
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 notifications controller for RhodeCode
7
8 :created_on: Nov 23, 2010
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 import logging
27 import traceback
28
29 from pylons import request
30 from pylons import tmpl_context as c, url
31 from pylons.controllers.util import redirect
32
33 from rhodecode.lib.base import BaseController, render
34 from rhodecode.model.db import Notification
35
36 from rhodecode.model.notification import NotificationModel
37 from rhodecode.lib.auth import LoginRequired, NotAnonymous
38 from rhodecode.lib import helpers as h
39 from rhodecode.model.meta import Session
40
41
42 log = logging.getLogger(__name__)
43
44
45 class NotificationsController(BaseController):
46 """REST Controller styled on the Atom Publishing Protocol"""
47 # To properly map this controller, ensure your config/routing.py
48 # file has a resource setup:
49 # map.resource('notification', 'notifications', controller='_admin/notifications',
50 # path_prefix='/_admin', name_prefix='_admin_')
51
52 @LoginRequired()
53 @NotAnonymous()
54 def __before__(self):
55 super(NotificationsController, self).__before__()
56
57 def index(self, format='html'):
58 """GET /_admin/notifications: All items in the collection"""
59 # url('notifications')
60 c.user = self.rhodecode_user
61 c.notifications = NotificationModel()\
62 .get_for_user(self.rhodecode_user.user_id)
63 return render('admin/notifications/notifications.html')
64
65 def mark_all_read(self):
66 if request.environ.get('HTTP_X_PARTIAL_XHR'):
67 nm = NotificationModel()
68 # mark all read
69 nm.mark_all_read_for_user(self.rhodecode_user.user_id)
70 Session.commit()
71 c.user = self.rhodecode_user
72 c.notifications = nm.get_for_user(self.rhodecode_user.user_id)
73 return render('admin/notifications/notifications_data.html')
74
75 def create(self):
76 """POST /_admin/notifications: Create a new item"""
77 # url('notifications')
78
79 def new(self, format='html'):
80 """GET /_admin/notifications/new: Form to create a new item"""
81 # url('new_notification')
82
83 def update(self, notification_id):
84 """PUT /_admin/notifications/id: Update an existing item"""
85 # Forms posted to this method should contain a hidden field:
86 # <input type="hidden" name="_method" value="PUT" />
87 # Or using helpers:
88 # h.form(url('notification', notification_id=ID),
89 # method='put')
90 # url('notification', notification_id=ID)
91
92 def delete(self, notification_id):
93 """DELETE /_admin/notifications/id: Delete an existing item"""
94 # Forms posted to this method should contain a hidden field:
95 # <input type="hidden" name="_method" value="DELETE" />
96 # Or using helpers:
97 # h.form(url('notification', notification_id=ID),
98 # method='delete')
99 # url('notification', notification_id=ID)
100
101 try:
102 no = Notification.get(notification_id)
103 owner = lambda: (no.notifications_to_users.user.user_id
104 == c.rhodecode_user.user_id)
105 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
106 NotificationModel().delete(c.rhodecode_user.user_id, no)
107 Session.commit()
108 return 'ok'
109 except Exception:
110 Session.rollback()
111 log.error(traceback.format_exc())
112 return 'fail'
113
114 def show(self, notification_id, format='html'):
115 """GET /_admin/notifications/id: Show a specific item"""
116 # url('notification', notification_id=ID)
117 c.user = self.rhodecode_user
118 no = Notification.get(notification_id)
119
120 owner = lambda: (no.notifications_to_users.user.user_id
121 == c.user.user_id)
122 if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner):
123 unotification = NotificationModel()\
124 .get_user_notification(c.user.user_id, no)
125
126 # if this association to user is not valid, we don't want to show
127 # this message
128 if unotification:
129 if unotification.read is False:
130 unotification.mark_as_read()
131 Session.commit()
132 c.notification = no
133
134 return render('admin/notifications/show_notification.html')
135
136 return redirect(url('notifications'))
137
138 def edit(self, notification_id, format='html'):
139 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
140 # url('edit_notification', notification_id=ID)
@@ -0,0 +1,57 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.bookmarks
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Bookmarks controller for rhodecode
7
8 :created_on: Dec 1, 2011
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
26
27 from pylons import tmpl_context as c
28
29 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
30 from rhodecode.lib.base import BaseRepoController, render
31 from rhodecode.lib.compat import OrderedDict
32 from webob.exc import HTTPNotFound
33
34 log = logging.getLogger(__name__)
35
36
37 class BookmarksController(BaseRepoController):
38
39 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 'repository.admin')
42 def __before__(self):
43 super(BookmarksController, self).__before__()
44
45 def index(self):
46 if c.rhodecode_repo.alias != 'hg':
47 raise HTTPNotFound()
48
49 c.repo_bookmarks = OrderedDict()
50
51 bookmarks = [(name, c.rhodecode_repo.get_changeset(hash_)) for \
52 name, hash_ in c.rhodecode_repo._repo._bookmarks.items()]
53 ordered_tags = sorted(bookmarks, key=lambda x: x[1].date, reverse=True)
54 for name, cs_book in ordered_tags:
55 c.repo_bookmarks[name] = cs_book
56
57 return render('bookmarks/bookmarks.html')
@@ -0,0 +1,190 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.annotate
4 ~~~~~~~~~~~~~~~~~~~~~~
5
6 Anontation library for usage in rhodecode, previously part of vcs
7
8 :created_on: Dec 4, 2011
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13
14 from rhodecode.lib.vcs.exceptions import VCSError
15 from rhodecode.lib.vcs.nodes import FileNode
16 from pygments.formatters import HtmlFormatter
17 from pygments import highlight
18
19 import StringIO
20
21
22 def annotate_highlight(filenode, annotate_from_changeset_func=None,
23 order=None, headers=None, **options):
24 """
25 Returns html portion containing annotated table with 3 columns: line
26 numbers, changeset information and pygmentized line of code.
27
28 :param filenode: FileNode object
29 :param annotate_from_changeset_func: function taking changeset and
30 returning single annotate cell; needs break line at the end
31 :param order: ordered sequence of ``ls`` (line numbers column),
32 ``annotate`` (annotate column), ``code`` (code column); Default is
33 ``['ls', 'annotate', 'code']``
34 :param headers: dictionary with headers (keys are whats in ``order``
35 parameter)
36 """
37 options['linenos'] = True
38 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
39 headers=headers,
40 annotate_from_changeset_func=annotate_from_changeset_func, **options)
41 lexer = filenode.lexer
42 highlighted = highlight(filenode.content, lexer, formatter)
43 return highlighted
44
45
46 class AnnotateHtmlFormatter(HtmlFormatter):
47
48 def __init__(self, filenode, annotate_from_changeset_func=None,
49 order=None, **options):
50 """
51 If ``annotate_from_changeset_func`` is passed it should be a function
52 which returns string from the given changeset. For example, we may pass
53 following function as ``annotate_from_changeset_func``::
54
55 def changeset_to_anchor(changeset):
56 return '<a href="/changesets/%s/">%s</a>\n' %\
57 (changeset.id, changeset.id)
58
59 :param annotate_from_changeset_func: see above
60 :param order: (default: ``['ls', 'annotate', 'code']``); order of
61 columns;
62 :param options: standard pygment's HtmlFormatter options, there is
63 extra option tough, ``headers``. For instance we can pass::
64
65 formatter = AnnotateHtmlFormatter(filenode, headers={
66 'ls': '#',
67 'annotate': 'Annotate',
68 'code': 'Code',
69 })
70
71 """
72 super(AnnotateHtmlFormatter, self).__init__(**options)
73 self.annotate_from_changeset_func = annotate_from_changeset_func
74 self.order = order or ('ls', 'annotate', 'code')
75 headers = options.pop('headers', None)
76 if headers and not ('ls' in headers and 'annotate' in headers and
77 'code' in headers):
78 raise ValueError("If headers option dict is specified it must "
79 "all 'ls', 'annotate' and 'code' keys")
80 self.headers = headers
81 if isinstance(filenode, FileNode):
82 self.filenode = filenode
83 else:
84 raise VCSError("This formatter expect FileNode parameter, not %r"
85 % type(filenode))
86
87 def annotate_from_changeset(self, changeset):
88 """
89 Returns full html line for single changeset per annotated line.
90 """
91 if self.annotate_from_changeset_func:
92 return self.annotate_from_changeset_func(changeset)
93 else:
94 return ''.join((changeset.id, '\n'))
95
96 def _wrap_tablelinenos(self, inner):
97 dummyoutfile = StringIO.StringIO()
98 lncount = 0
99 for t, line in inner:
100 if t:
101 lncount += 1
102 dummyoutfile.write(line)
103
104 fl = self.linenostart
105 mw = len(str(lncount + fl - 1))
106 sp = self.linenospecial
107 st = self.linenostep
108 la = self.lineanchors
109 aln = self.anchorlinenos
110 if sp:
111 lines = []
112
113 for i in range(fl, fl + lncount):
114 if i % st == 0:
115 if i % sp == 0:
116 if aln:
117 lines.append('<a href="#%s-%d" class="special">'
118 '%*d</a>' %
119 (la, i, mw, i))
120 else:
121 lines.append('<span class="special">'
122 '%*d</span>' % (mw, i))
123 else:
124 if aln:
125 lines.append('<a href="#%s-%d">'
126 '%*d</a>' % (la, i, mw, i))
127 else:
128 lines.append('%*d' % (mw, i))
129 else:
130 lines.append('')
131 ls = '\n'.join(lines)
132 else:
133 lines = []
134 for i in range(fl, fl + lncount):
135 if i % st == 0:
136 if aln:
137 lines.append('<a href="#%s-%d">%*d</a>' \
138 % (la, i, mw, i))
139 else:
140 lines.append('%*d' % (mw, i))
141 else:
142 lines.append('')
143 ls = '\n'.join(lines)
144
145 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
146 # If pygments cropped last lines break we need do that too
147 ln_cs = len(annotate_changesets)
148 ln_ = len(ls.splitlines())
149 if ln_cs > ln_:
150 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
151 annotate = ''.join((self.annotate_from_changeset(changeset)
152 for changeset in annotate_changesets))
153 # in case you wonder about the seemingly redundant <div> here:
154 # since the content in the other cell also is wrapped in a div,
155 # some browsers in some configurations seem to mess up the formatting.
156 '''
157 yield 0, ('<table class="%stable">' % self.cssclass +
158 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
159 ls + '</pre></div></td>' +
160 '<td class="code">')
161 yield 0, dummyoutfile.getvalue()
162 yield 0, '</td></tr></table>'
163
164 '''
165 headers_row = []
166 if self.headers:
167 headers_row = ['<tr class="annotate-header">']
168 for key in self.order:
169 td = ''.join(('<td>', self.headers[key], '</td>'))
170 headers_row.append(td)
171 headers_row.append('</tr>')
172
173 body_row_start = ['<tr>']
174 for key in self.order:
175 if key == 'ls':
176 body_row_start.append(
177 '<td class="linenos"><div class="linenodiv"><pre>' +
178 ls + '</pre></div></td>')
179 elif key == 'annotate':
180 body_row_start.append(
181 '<td class="annotate"><div class="annotatediv"><pre>' +
182 annotate + '</pre></div></td>')
183 elif key == 'code':
184 body_row_start.append('<td class="code">')
185 yield 0, ('<table class="%stable">' % self.cssclass +
186 ''.join(headers_row) +
187 ''.join(body_row_start)
188 )
189 yield 0, dummyoutfile.getvalue()
190 yield 0, '</td></tr></table>'
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,17 +1,21 b''
1 1 syntax: glob
2 2 *.pyc
3 3 *.swp
4 *.sqlite
4 5 *.egg-info
5 6 *.egg
6 7
7 8 syntax: regexp
8 9 ^build
9 10 ^docs/build/
10 11 ^docs/_build/
11 12 ^data$
12 13 ^\.settings$
13 14 ^\.project$
14 15 ^\.pydevproject$
16 ^\.coverage$
15 17 ^rhodecode\.db$
16 18 ^test\.db$
17 ^repositories\.config$
19 ^RhodeCode\.egg-info$
20 ^rc\.ini$
21 ^fabfile.py
@@ -1,15 +1,18 b''
1 1 List of contributors to RhodeCode project:
2 2 Marcin Kuźmiński <marcin@python-works.com>
3 3 Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
4 4 Jason Harris <jason@jasonfharris.com>
5 5 Thayne Harbaugh <thayne@fusionio.com>
6 6 cejones
7 7 Thomas Waldmann <tw-public@gmx.de>
8 8 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
9 9 Dmitri Kuznetsov
10 10 Jared Bunting <jared.bunting@peachjean.com>
11 11 Steve Romanow <slestak989@gmail.com>
12 12 Augosto Hermann <augusto.herrmann@planejamento.gov.br>
13 13 Ankit Solanki <ankit.solanki@gmail.com>
14 14 Liad Shani <liadff@gmail.com>
15 15 Les Peabody <lpeabody@gmail.com>
16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
17 Matt Zuba <matt.zuba@goodwillaz.org>
18 Aras Pranckevicius <aras@unity3d.com> No newline at end of file
@@ -1,149 +1,173 b''
1 =================================================
2 Welcome to RhodeCode (RhodiumCode) documentation!
3 =================================================
1 =========
2 RhodeCode
3 =========
4 4
5 ``RhodeCode`` is a Pylons framework based Mercurial repository
6 browser/management tool with a built in push/pull server and full text search.
5 About
6 -----
7
8 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
9 with a built in push/pull server and full text search and code-review.
7 10 It works on http/https and has a built in permission/authentication system with
8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
9 simple API so it's easy integrable with existing systems.
11 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
12 simple API so it's easy integrable with existing external systems.
10 13
11 14 RhodeCode is similar in some respects to github or bitbucket_,
12 however RhodeCode can be run as standalone hosted application on your own server.
15 however RhodeCode can be run as standalone hosted application on your own server.
13 16 It is open source and donation ware and focuses more on providing a customized,
14 self administered interface for Mercurial(and soon GIT) repositories.
17 self administered interface for Mercurial and GIT repositories.
15 18 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
16 19 handle multiple different version control systems.
17 20
18 21 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
19 22
23 Installation
24 ------------
25 Stable releases of RhodeCode are best installed via::
26
27 easy_install rhodecode
28
29 Or::
30
31 pip install rhodecode
32
33 Detailed instructions and links may be found on the Installation page.
34
35 Please visit http://packages.python.org/RhodeCode/installation.html for
36 more details
37
20 38 RhodeCode demo
21 39 --------------
22 40
23 41 http://demo.rhodecode.org
24 42
25 43 The default access is anonymous but you can login to an administrative account
26 44 using the following credentials:
27 45
28 46 - username: demo
29 47 - password: demo12
30 48
31 49 Source code
32 50 -----------
33 51
34 52 The latest sources can be obtained from official RhodeCode instance
35 53 https://secure.rhodecode.org
36 54
37 55
38 56 MIRRORS:
39 57
40 58 Issue tracker and sources at bitbucket_
41 59
42 60 http://bitbucket.org/marcinkuzminski/rhodecode
43 61
44 62 Sources at github_
45 63
46 64 https://github.com/marcinkuzminski/rhodecode
47 65
48 Installation
49 ------------
50
51 Please visit http://packages.python.org/RhodeCode/installation.html
52
53 66
54 67 RhodeCode Features
55 68 ------------------
56 69
57 - Has it's own middleware to handle mercurial_ protocol requests.
70 - Has its own middleware to handle mercurial_ protocol requests.
58 71 Each request can be logged and authenticated.
59 72 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
60 73 Supports http/https and LDAP
61 74 - Full permissions (private/read/write/admin) and authentication per project.
62 75 One account for web interface and mercurial_ push/pull/clone operations.
63 76 - Have built in users groups for easier permission management
64 77 - Repository groups let you group repos and manage them easier.
65 78 - Users can fork other users repo. RhodeCode have also compare view to see
66 79 combined changeset for all changeset made within single push.
67 80 - Build in commit-api let's you add, edit and commit files right from RhodeCode
68 81 interface using simple editor or upload form for binaries.
69 82 - Mako templates let's you customize the look and feel of the application.
70 83 - Beautiful diffs, annotations and source code browsing all colored by pygments.
71 84 Raw diffs are made in git-diff format, including git_ binary-patches
72 85 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
73 86 - Admin interface with user/permission management. Admin activity journal, logs
74 87 pulls, pushes, forks, registrations and other actions made by all users.
75 88 - Server side forks. It is possible to fork a project and modify it freely
76 89 without breaking the main repository. You can even write Your own hooks
77 90 and install them
91 - code review with notification system, inline commenting, all parsed using
92 rst syntax
93 - rst and markdown README support for repositories
78 94 - Full text search powered by Whoosh on the source files, and file names.
79 95 Build in indexing daemons, with optional incremental index build
80 96 (no external search servers required all in one application)
81 97 - Setup project descriptions and info inside built in db for easy, non
82 98 file-system operations
83 99 - Intelligent cache with invalidation after push or project change, provides
84 100 high performance and always up to date data.
85 101 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
86 102 - Async tasks for speed and performance using celery_ (works without them too)
87 103 - Backup scripts can do backup of whole app and send it over scp to desired
88 104 location
89 105 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
90 106
91
92 .. include:: ./docs/screenshots.rst
93
94 107
95 108 Incoming / Plans
96 109 ----------------
97 110
98 111 - Finer granular permissions per branch, repo group or subrepo
99 112 - pull requests and web based merges
100 - notification and message system
113 - per line file history
101 114 - SSH based authentication with server side key management
102 - Code review (probably based on hg-review)
103 - Full git_ support, with push/pull server (currently in beta tests)
104 - Redmine and other bugtrackers integration
105 115 - Commit based built in wiki system
106 116 - More statistics and graph (global annotation + some more statistics)
107 117 - Other advancements as development continues (or you can of course make
108 118 additions and or requests)
109 119
110 120 License
111 121 -------
112 122
113 123 ``RhodeCode`` is released under the GPLv3 license.
114 124
115 125
116 Mailing group Q&A
117 -----------------
126 Getting help
127 ------------
118 128
119 Join the `Google group <http://groups.google.com/group/rhodecode>`_
129 Listed bellow are various support resources that should help.
120 130
121 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
131 .. note::
132
133 Please try to read the documentation before posting any issues
134
135 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
136 any questions.
122 137
123 Join #rhodecode on FreeNode (irc.freenode.net)
124 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
138 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
139
140
141 - Join #rhodecode on FreeNode (irc.freenode.net)
142 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
143
144 - You can also follow me on twitter @marcinkuzminski where i often post some
145 news about RhodeCode
146
125 147
126 148 Online documentation
127 149 --------------------
128 150
129 151 Online documentation for the current version of RhodeCode is available at
130 http://packages.python.org/RhodeCode/.
152 - http://packages.python.org/RhodeCode/
153 - http://rhodecode.readthedocs.org/en/latest/index.html
154
131 155 You may also build the documentation for yourself - go into ``docs/`` and run::
132 156
133 157 make html
134 158
135 159 (You need to have sphinx_ installed to build the documentation. If you don't
136 160 have sphinx_ installed you can install it via the command:
137 161 ``easy_install sphinx``)
138 162
139 163 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
140 164 .. _python: http://www.python.org/
141 165 .. _sphinx: http://sphinx.pocoo.org/
142 166 .. _mercurial: http://mercurial.selenic.com/
143 167 .. _bitbucket: http://bitbucket.org/
144 168 .. _github: http://github.com/
145 169 .. _subversion: http://subversion.tigris.org/
146 170 .. _git: http://git-scm.com/
147 171 .. _celery: http://celeryproject.org/
148 172 .. _Sphinx: http://sphinx.pocoo.org/
149 173 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,235 +1,294 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
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 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 #email_prefix = [RhodeCode]
20 21
21 22 #smtp_server = mail.server.com
22 23 #smtp_username =
23 24 #smtp_password =
24 25 #smtp_port =
25 26 #smtp_use_tls = false
26 27 #smtp_use_ssl = true
27 28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 29 #smtp_auth =
29 30
30 31 [server:main]
31 32 ##nr of threads to spawn
32 33 threadpool_workers = 5
33 34
34 35 ##max request before thread respawn
35 threadpool_max_requests = 6
36 threadpool_max_requests = 10
36 37
37 38 ##option to use threads of process
38 39 use_threadpool = true
39 40
40 41 use = egg:Paste#http
41 42 host = 0.0.0.0
42 43 port = 5000
43 44
44 45 [app:main]
45 46 use = egg:rhodecode
46 47 full_stack = true
47 48 static_files = true
48 lang=en
49 lang = en
49 50 cache_dir = %(here)s/data
50 51 index_dir = %(here)s/data/index
51 app_instance_uuid = develop
52 app_instance_uuid = rc-develop
52 53 cut_off_limit = 256000
53 54 force_https = false
54 55 commit_parse_limit = 25
55 56 use_gravatar = true
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
59 default_encoding = utf8
60
61 ## overwrite schema of clone url
62 ## available vars:
63 ## scheme - http/https
64 ## user - current user
65 ## pass - password
66 ## netloc - network location
67 ## path - usually repo_name
68
69 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70
71 ## issue tracking mapping for commits messages
72 ## comment out issue_pat, issue_server, issue_prefix to enable
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #<numbers> with a regex passive group for `#`
76 ## {id} will be all groups matched from this pattern
77
78 issue_pat = (?:\s*#)(\d+)
79
80 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
82
83 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84
85 ## prefix to add to link to indicate it's an url
86 ## #314 will be replaced by <issue_prefix><id>
87
88 issue_prefix = #
89
90 ## instance-id prefix
91 ## a prefix key for this instance used for cache invalidation when running
92 ## multiple instances of rhodecode, make sure it's globally unique for
93 ## all running rhodecode instances. Leave empty if you don't use it
94 instance_id =
56 95
57 96 ####################################
58 97 ### CELERY CONFIG ####
59 98 ####################################
60 99 use_celery = false
61 100 broker.host = localhost
62 101 broker.vhost = rabbitmqhost
63 102 broker.port = 5672
64 103 broker.user = rabbitmq
65 104 broker.password = qweqwe
66 105
67 106 celery.imports = rhodecode.lib.celerylib.tasks
68 107
69 108 celery.result.backend = amqp
70 109 celery.result.dburi = amqp://
71 110 celery.result.serialier = json
72 111
73 112 #celery.send.task.error.emails = true
74 113 #celery.amqp.task.result.expires = 18000
75 114
76 115 celeryd.concurrency = 2
77 116 #celeryd.log.file = celeryd.log
78 117 celeryd.log.level = debug
79 118 celeryd.max.tasks.per.child = 1
80 119
81 120 #tasks will never be sent to the queue, but executed locally instead.
82 121 celery.always.eager = false
83 122
84 123 ####################################
85 124 ### BEAKER CACHE ####
86 125 ####################################
87 126 beaker.cache.data_dir=%(here)s/data/cache/data
88 127 beaker.cache.lock_dir=%(here)s/data/cache/lock
89 128
90 129 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
91 130
92 131 beaker.cache.super_short_term.type=memory
93 132 beaker.cache.super_short_term.expire=10
133 beaker.cache.super_short_term.key_length = 256
94 134
95 135 beaker.cache.short_term.type=memory
96 136 beaker.cache.short_term.expire=60
137 beaker.cache.short_term.key_length = 256
97 138
98 139 beaker.cache.long_term.type=memory
99 140 beaker.cache.long_term.expire=36000
141 beaker.cache.long_term.key_length = 256
100 142
101 143 beaker.cache.sql_cache_short.type=memory
102 144 beaker.cache.sql_cache_short.expire=10
145 beaker.cache.sql_cache_short.key_length = 256
103 146
104 147 beaker.cache.sql_cache_med.type=memory
105 148 beaker.cache.sql_cache_med.expire=360
149 beaker.cache.sql_cache_med.key_length = 256
106 150
107 151 beaker.cache.sql_cache_long.type=file
108 152 beaker.cache.sql_cache_long.expire=3600
153 beaker.cache.sql_cache_long.key_length = 256
109 154
110 155 ####################################
111 156 ### BEAKER SESSION ####
112 157 ####################################
113 158 ## Type of storage used for the session, current types are
114 159 ## dbm, file, memcached, database, and memory.
115 160 ## The storage uses the Container API
116 ##that is also used by the cache system.
117 beaker.session.type = file
161 ## that is also used by the cache system.
162
163 ## db session example
164
165 #beaker.session.type = ext:database
166 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
167 #beaker.session.table_name = db_session
118 168
169 ## encrypted cookie session, good for many instances
170 #beaker.session.type = cookie
171
172 beaker.session.type = file
119 173 beaker.session.key = rhodecode
120 beaker.session.secret = g654dcno0-9873jhgfreyu
174 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
175 #beaker.session.validate_key = 9712sds2212c--zxc123
121 176 beaker.session.timeout = 36000
177 beaker.session.httponly = true
178
179 ## uncomment for https secure cookie
180 beaker.session.secure = false
122 181
123 182 ##auto save the session to not to use .save()
124 183 beaker.session.auto = False
125 184
126 185 ##true exire at browser close
127 186 #beaker.session.cookie_expires = 3600
128 187
129
188
130 189 ################################################################################
131 190 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
132 191 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
133 192 ## execute malicious code after an exception is raised. ##
134 193 ################################################################################
135 194 #set debug = false
136 195
137 196 ##################################
138 197 ### LOGVIEW CONFIG ###
139 198 ##################################
140 199 logview.sqlalchemy = #faa
141 200 logview.pylons.templating = #bfb
142 201 logview.pylons.util = #eee
143 202
144 203 #########################################################
145 204 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
146 205 #########################################################
147 206 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
148 207 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
149 208 sqlalchemy.db1.echo = false
150 209 sqlalchemy.db1.pool_recycle = 3600
151 210 sqlalchemy.convert_unicode = true
152 211
153 212 ################################
154 213 ### LOGGING CONFIGURATION ####
155 214 ################################
156 215 [loggers]
157 216 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
158 217
159 218 [handlers]
160 219 keys = console, console_sql
161 220
162 221 [formatters]
163 222 keys = generic, color_formatter, color_formatter_sql
164 223
165 224 #############
166 225 ## LOGGERS ##
167 226 #############
168 227 [logger_root]
169 228 level = NOTSET
170 229 handlers = console
171 230
172 231 [logger_routes]
173 232 level = DEBUG
174 233 handlers =
175 234 qualname = routes.middleware
176 235 # "level = DEBUG" logs the route matched and routing variables.
177 236 propagate = 1
178 237
179 238 [logger_beaker]
180 239 level = DEBUG
181 240 handlers =
182 241 qualname = beaker.container
183 242 propagate = 1
184 243
185 244 [logger_templates]
186 245 level = INFO
187 246 handlers =
188 247 qualname = pylons.templating
189 248 propagate = 1
190 249
191 250 [logger_rhodecode]
192 251 level = DEBUG
193 252 handlers =
194 253 qualname = rhodecode
195 254 propagate = 1
196 255
197 256 [logger_sqlalchemy]
198 257 level = INFO
199 258 handlers = console_sql
200 259 qualname = sqlalchemy.engine
201 260 propagate = 0
202 261
203 262 ##############
204 263 ## HANDLERS ##
205 264 ##############
206 265
207 266 [handler_console]
208 267 class = StreamHandler
209 268 args = (sys.stderr,)
210 269 level = DEBUG
211 270 formatter = color_formatter
212 271
213 272 [handler_console_sql]
214 273 class = StreamHandler
215 274 args = (sys.stderr,)
216 275 level = DEBUG
217 276 formatter = color_formatter_sql
218 277
219 278 ################
220 279 ## FORMATTERS ##
221 280 ################
222 281
223 282 [formatter_generic]
224 283 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
225 284 datefmt = %Y-%m-%d %H:%M:%S
226 285
227 286 [formatter_color_formatter]
228 287 class=rhodecode.lib.colored_formatter.ColorFormatter
229 288 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
230 289 datefmt = %Y-%m-%d %H:%M:%S
231 290
232 291 [formatter_color_formatter_sql]
233 292 class=rhodecode.lib.colored_formatter.ColorFormatterSql
234 293 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
235 294 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,392 +1,627 b''
1 1 .. _api:
2 2
3 3
4 4 API
5 5 ===
6 6
7 7
8 8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 9 There's a single schema for calling all api methods. API is implemented
10 10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 11 <your_server>/_admin/api
12 12
13 13 API ACCESS FOR WEB VIEWS
14 14 ++++++++++++++++++++++++
15 15
16 16 API access can also be turned on for each web view in RhodeCode that is
17 17 decorated with `@LoginRequired` decorator. To enable API access simple change
18 18 the standard login decorator to `@LoginRequired(api_access=True)`.
19 19 After this change, a rhodecode view can be accessed without login by adding a
20 20 GET parameter `?api_key=<api_key>` to url. By default this is only
21 21 enabled on RSS/ATOM feed views.
22 22
23 23
24 24 API ACCESS
25 25 ++++++++++
26 26
27 27 All clients are required to send JSON-RPC spec JSON data::
28 28
29 {
29 {
30 "id:<id>,
30 31 "api_key":"<api_key>",
31 32 "method":"<method_name>",
32 33 "args":{"<arg_key>":"<arg_val>"}
33 34 }
34 35
35 36 Example call for autopulling remotes repos using curl::
36 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
37 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
37 38
38 39 Simply provide
40 - *id* A value of any type, which is used to match the response with the request that it is replying to.
39 41 - *api_key* for access and permission validation.
40 42 - *method* is name of method to call
41 43 - *args* is an key:value list of arguments to pass to method
42 44
43 45 .. note::
44 46
45 47 api_key can be found in your user account page
46 48
47 49
48 50 RhodeCode API will return always a JSON-RPC response::
49 51
50 {
52 {
53 "id":<id>,
51 54 "result": "<result>",
52 55 "error": null
53 56 }
54 57
55 58 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
56 59 calling api *error* key from response will contain failure description
57 60 and result will be null.
58 61
59 62 API METHODS
60 63 +++++++++++
61 64
62 65
63 66 pull
64 67 ----
65 68
66 69 Pulls given repo from remote location. Can be used to automatically keep
67 70 remote repos up to date. This command can be executed only using api_key
68 71 belonging to user with admin rights
69 72
70 73 INPUT::
71 74
72 75 api_key : "<api_key>"
73 76 method : "pull"
74 77 args : {
75 "repo" : "<repo_name>"
78 "repo_name" : "<reponame>"
76 79 }
77 80
78 81 OUTPUT::
79 82
80 result : "Pulled from <repo_name>"
83 result : "Pulled from <reponame>"
81 84 error : null
82 85
83 86
87 get_user
88 --------
89
90 Get's an user by username or user_id, Returns empty result if user is not found.
91 This command can be executed only using api_key belonging to user with admin
92 rights.
93
94
95 INPUT::
96
97 api_key : "<api_key>"
98 method : "get_user"
99 args : {
100 "userid" : "<username or user_id>"
101 }
102
103 OUTPUT::
104
105 result: None if user does not exist or
106 {
107 "id" : "<id>",
108 "username" : "<username>",
109 "firstname": "<firstname>",
110 "lastname" : "<lastname>",
111 "email" : "<email>",
112 "active" : "<bool>",
113 "admin" :  "<bool>",
114 "ldap_dn" : "<ldap_dn>"
115 }
116
117 error: null
118
119
84 120 get_users
85 121 ---------
86 122
87 123 Lists all existing users. This command can be executed only using api_key
88 124 belonging to user with admin rights.
89 125
126
90 127 INPUT::
91 128
92 129 api_key : "<api_key>"
93 130 method : "get_users"
94 131 args : { }
95 132
96 133 OUTPUT::
97 134
98 135 result: [
99 136 {
100 137 "id" : "<id>",
101 138 "username" : "<username>",
102 139 "firstname": "<firstname>",
103 140 "lastname" : "<lastname>",
104 141 "email" : "<email>",
105 142 "active" : "<bool>",
106 143 "admin" :  "<bool>",
107 "ldap" : "<ldap_dn>"
144 "ldap_dn" : "<ldap_dn>"
108 145 },
109 146
110 147 ]
111 148 error: null
112 149
150
113 151 create_user
114 152 -----------
115 153
116 Creates new user or updates current one if such user exists. This command can
154 Creates new user. This command can
117 155 be executed only using api_key belonging to user with admin rights.
118 156
157
119 158 INPUT::
120 159
121 160 api_key : "<api_key>"
122 161 method : "create_user"
123 162 args : {
124 163 "username" : "<username>",
125 164 "password" : "<password>",
126 "firstname" : "<firstname>",
127 "lastname" : "<lastname>",
128 "email" : "<useremail>"
165 "email" : "<useremail>",
166 "firstname" : "<firstname> = None",
167 "lastname" : "<lastname> = None",
129 168 "active" : "<bool> = True",
130 169 "admin" : "<bool> = False",
131 170 "ldap_dn" : "<ldap_dn> = None"
132 171 }
133 172
134 173 OUTPUT::
135 174
136 175 result: {
176 "id" : "<new_user_id>",
137 177 "msg" : "created new user <username>"
138 178 }
139 179 error: null
140 180
181
182 update_user
183 -----------
184
185 updates current one if such user exists. This command can
186 be executed only using api_key belonging to user with admin rights.
187
188
189 INPUT::
190
191 api_key : "<api_key>"
192 method : "update_user"
193 args : {
194 "userid" : "<user_id or username>",
195 "username" : "<username>",
196 "password" : "<password>",
197 "email" : "<useremail>",
198 "firstname" : "<firstname>",
199 "lastname" : "<lastname>",
200 "active" : "<bool>",
201 "admin" : "<bool>",
202 "ldap_dn" : "<ldap_dn>"
203 }
204
205 OUTPUT::
206
207 result: {
208 "id" : "<edited_user_id>",
209 "msg" : "updated user <username>"
210 }
211 error: null
212
213
214 get_users_group
215 ---------------
216
217 Gets an existing users group. This command can be executed only using api_key
218 belonging to user with admin rights.
219
220
221 INPUT::
222
223 api_key : "<api_key>"
224 method : "get_users_group"
225 args : {
226 "group_name" : "<name>"
227 }
228
229 OUTPUT::
230
231 result : None if group not exist
232 {
233 "id" : "<id>",
234 "group_name" : "<groupname>",
235 "active": "<bool>",
236 "members" : [
237 { "id" : "<userid>",
238 "username" : "<username>",
239 "firstname": "<firstname>",
240 "lastname" : "<lastname>",
241 "email" : "<email>",
242 "active" : "<bool>",
243 "admin" :  "<bool>",
244 "ldap" : "<ldap_dn>"
245 },
246
247 ]
248 }
249 error : null
250
251
141 252 get_users_groups
142 253 ----------------
143 254
144 Lists all existing users groups. This command can be executed only using api_key
145 belonging to user with admin rights.
255 Lists all existing users groups. This command can be executed only using
256 api_key belonging to user with admin rights.
257
146 258
147 259 INPUT::
148 260
149 261 api_key : "<api_key>"
150 262 method : "get_users_groups"
151 263 args : { }
152 264
153 265 OUTPUT::
154 266
155 267 result : [
156 268 {
157 "id" : "<id>",
158 "name" : "<name>",
159 "active": "<bool>",
269 "id" : "<id>",
270 "group_name" : "<groupname>",
271 "active": "<bool>",
160 272 "members" : [
161 273 {
162 274 "id" : "<userid>",
163 275 "username" : "<username>",
164 276 "firstname": "<firstname>",
165 277 "lastname" : "<lastname>",
166 278 "email" : "<email>",
167 279 "active" : "<bool>",
168 280 "admin" :  "<bool>",
169 281 "ldap" : "<ldap_dn>"
170 282 },
171 283
172 284 ]
173 285 }
174 286 ]
175 287 error : null
176 288
177 get_users_group
178 ---------------
179
180 Gets an existing users group. This command can be executed only using api_key
181 belonging to user with admin rights.
182
183 INPUT::
184
185 api_key : "<api_key>"
186 method : "get_users_group"
187 args : {
188 "group_name" : "<name>"
189 }
190
191 OUTPUT::
192
193 result : None if group not exist
194 {
195 "id" : "<id>",
196 "name" : "<name>",
197 "active": "<bool>",
198 "members" : [
199 { "id" : "<userid>",
200 "username" : "<username>",
201 "firstname": "<firstname>",
202 "lastname" : "<lastname>",
203 "email" : "<email>",
204 "active" : "<bool>",
205 "admin" :  "<bool>",
206 "ldap" : "<ldap_dn>"
207 },
208
209 ]
210 }
211 error : null
212 289
213 290 create_users_group
214 291 ------------------
215 292
216 293 Creates new users group. This command can be executed only using api_key
217 294 belonging to user with admin rights
218 295
296
219 297 INPUT::
220 298
221 299 api_key : "<api_key>"
222 300 method : "create_users_group"
223 301 args: {
224 "name": "<name>",
302 "group_name": "<groupname>",
225 303 "active":"<bool> = True"
226 304 }
227 305
228 306 OUTPUT::
229 307
230 308 result: {
231 309 "id": "<newusersgroupid>",
232 "msg": "created new users group <name>"
310 "msg": "created new users group <groupname>"
233 311 }
234 312 error: null
235 313
314
236 315 add_user_to_users_group
237 316 -----------------------
238 317
239 Adds a user to a users group. This command can be executed only using api_key
318 Adds a user to a users group. If user exists in that group success will be
319 `false`. This command can be executed only using api_key
240 320 belonging to user with admin rights
241 321
322
242 323 INPUT::
243 324
244 325 api_key : "<api_key>"
245 326 method : "add_user_users_group"
246 327 args: {
247 328 "group_name" : "<groupname>",
248 "user_name" : "<username>"
329 "username" : "<username>"
249 330 }
250 331
251 332 OUTPUT::
252 333
253 334 result: {
254 335 "id": "<newusersgroupmemberid>",
255 "msg": "created new users group member"
336 "success": True|False # depends on if member is in group
337 "msg": "added member <username> to users group <groupname> |
338 User is already in that group"
339 }
340 error: null
341
342
343 remove_user_from_users_group
344 ----------------------------
345
346 Removes a user from a users group. If user is not in given group success will
347 be `false`. This command can be executed only
348 using api_key belonging to user with admin rights
349
350
351 INPUT::
352
353 api_key : "<api_key>"
354 method : "remove_user_from_users_group"
355 args: {
356 "group_name" : "<groupname>",
357 "username" : "<username>"
358 }
359
360 OUTPUT::
361
362 result: {
363 "success": True|False, # depends on if member is in group
364 "msg": "removed member <username> from users group <groupname> |
365 User wasn't in group"
256 366 }
257 367 error: null
258 368
369
370 get_repo
371 --------
372
373 Gets an existing repository by it's name or repository_id. This command can
374 be executed only using api_key belonging to user with admin rights.
375
376
377 INPUT::
378
379 api_key : "<api_key>"
380 method : "get_repo"
381 args: {
382 "repoid" : "<reponame or repo_id>"
383 }
384
385 OUTPUT::
386
387 result: None if repository does not exist or
388 {
389 "id" : "<id>",
390 "repo_name" : "<reponame>"
391 "type" : "<type>",
392 "description" : "<description>",
393 "members" : [
394 { "id" : "<userid>",
395 "username" : "<username>",
396 "firstname": "<firstname>",
397 "lastname" : "<lastname>",
398 "email" : "<email>",
399 "active" : "<bool>",
400 "admin" :  "<bool>",
401 "ldap" : "<ldap_dn>",
402 "permission" : "repository.(read|write|admin)"
403 },
404
405 {
406 "id" : "<usersgroupid>",
407 "name" : "<usersgroupname>",
408 "active": "<bool>",
409 "permission" : "repository.(read|write|admin)"
410 },
411
412 ]
413 }
414 error: null
415
416
259 417 get_repos
260 418 ---------
261 419
262 420 Lists all existing repositories. This command can be executed only using api_key
263 421 belonging to user with admin rights
264 422
423
265 424 INPUT::
266 425
267 426 api_key : "<api_key>"
268 427 method : "get_repos"
269 428 args: { }
270 429
271 430 OUTPUT::
272 431
273 432 result: [
274 433 {
275 434 "id" : "<id>",
276 "name" : "<name>"
435 "repo_name" : "<reponame>"
277 436 "type" : "<type>",
278 437 "description" : "<description>"
279 438 },
280 439
281 440 ]
282 441 error: null
283 442
284 get_repo
285 --------
443
444 get_repo_nodes
445 --------------
286 446
287 Gets an existing repository. This command can be executed only using api_key
288 belonging to user with admin rights
447 returns a list of nodes and it's children in a flat list for a given path
448 at given revision. It's possible to specify ret_type to show only `files` or
449 `dirs`. This command can be executed only using api_key belonging to user
450 with admin rights
451
289 452
290 453 INPUT::
291 454
292 455 api_key : "<api_key>"
293 method : "get_repo"
456 method : "get_repo_nodes"
294 457 args: {
295 "name" : "<name>"
458 "repo_name" : "<reponame>",
459 "revision" : "<revision>",
460 "root_path" : "<root_path>",
461 "ret_type" : "<ret_type>" = 'all'
296 462 }
297 463
298 464 OUTPUT::
299 465
300 result: None if repository not exist
301 {
302 "id" : "<id>",
466 result: [
467 {
303 468 "name" : "<name>"
304 469 "type" : "<type>",
305 "description" : "<description>",
306 "members" : [
307 { "id" : "<userid>",
308 "username" : "<username>",
309 "firstname": "<firstname>",
310 "lastname" : "<lastname>",
311 "email" : "<email>",
312 "active" : "<bool>",
313 "admin" :  "<bool>",
314 "ldap" : "<ldap_dn>",
315 "permission" : "repository.(read|write|admin)"
316 },
317
318 {
319 "id" : "<usersgroupid>",
320 "name" : "<usersgroupname>",
321 "active": "<bool>",
322 "permission" : "repository.(read|write|admin)"
323 },
324
325 ]
326 }
470 },
471
472 ]
327 473 error: null
328 474
475
329 476 create_repo
330 477 -----------
331 478
332 479 Creates a repository. This command can be executed only using api_key
333 480 belonging to user with admin rights.
334 481 If repository name contains "/", all needed repository groups will be created.
335 482 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
336 483 and create "baz" repository with "bar" as group.
337 484
485
338 486 INPUT::
339 487
340 488 api_key : "<api_key>"
341 489 method : "create_repo"
342 490 args: {
343 "name" : "<name>",
491 "repo_name" : "<reponame>",
344 492 "owner_name" : "<ownername>",
345 493 "description" : "<description> = ''",
346 494 "repo_type" : "<type> = 'hg'",
347 "private" : "<bool> = False"
495 "private" : "<bool> = False",
496 "clone_uri" : "<clone_uri> = None",
348 497 }
349 498
350 499 OUTPUT::
351 500
352 result: None
501 result: {
502 "id": "<newrepoid>",
503 "msg": "Created new repository <reponame>",
504 }
353 505 error: null
354 506
355 add_user_to_repo
356 ----------------
507
508 delete_repo
509 -----------
510
511 Deletes a repository. This command can be executed only using api_key
512 belonging to user with admin rights.
513
514
515 INPUT::
516
517 api_key : "<api_key>"
518 method : "delete_repo"
519 args: {
520 "repo_name" : "<reponame>",
521 }
357 522
358 Add a user to a repository. This command can be executed only using api_key
359 belonging to user with admin rights.
360 If "perm" is None, user will be removed from the repository.
523 OUTPUT::
524
525 result: {
526 "msg": "Deleted repository <reponame>",
527 }
528 error: null
529
530
531 grant_user_permission
532 ---------------------
533
534 Grant permission for user on given repository, or update existing one
535 if found. This command can be executed only using api_key belonging to user
536 with admin rights.
537
361 538
362 539 INPUT::
363 540
364 541 api_key : "<api_key>"
365 method : "add_user_to_repo"
542 method : "grant_user_permission"
366 543 args: {
367 544 "repo_name" : "<reponame>",
368 "user_name" : "<username>",
369 "perm" : "(None|repository.(read|write|admin))",
545 "username" : "<username>",
546 "perm" : "(repository.(none|read|write|admin))",
547 }
548
549 OUTPUT::
550
551 result: {
552 "msg" : "Granted perm: <perm> for user: <username> in repo: <reponame>"
553 }
554 error: null
555
556
557 revoke_user_permission
558 ----------------------
559
560 Revoke permission for user on given repository. This command can be executed
561 only using api_key belonging to user with admin rights.
562
563
564 INPUT::
565
566 api_key : "<api_key>"
567 method : "revoke_user_permission"
568 args: {
569 "repo_name" : "<reponame>",
570 "username" : "<username>",
370 571 }
371 572
372 573 OUTPUT::
373 574
374 result: None
575 result: {
576 "msg" : "Revoked perm for user: <suername> in repo: <reponame>"
577 }
375 578 error: null
376 579
377 add_users_group_to_repo
378 -----------------------
580
581 grant_users_group_permission
582 ----------------------------
379 583
380 Add a users group to a repository. This command can be executed only using
381 api_key belonging to user with admin rights. If "perm" is None, group will
382 be removed from the repository.
584 Grant permission for users group on given repository, or update
585 existing one if found. This command can be executed only using
586 api_key belonging to user with admin rights.
587
383 588
384 589 INPUT::
385 590
386 591 api_key : "<api_key>"
387 method : "add_users_group_to_repo"
592 method : "grant_users_group_permission"
593 args: {
594 "repo_name" : "<reponame>",
595 "group_name" : "<usersgroupname>",
596 "perm" : "(repository.(none|read|write|admin))",
597 }
598
599 OUTPUT::
600
601 result: {
602 "msg" : "Granted perm: <perm> for group: <usersgroupname> in repo: <reponame>"
603 }
604 error: null
605
606
607 revoke_users_group_permission
608 -----------------------------
609
610 Revoke permission for users group on given repository.This command can be
611 executed only using api_key belonging to user with admin rights.
612
613 INPUT::
614
615 api_key : "<api_key>"
616 method : "revoke_users_group_permission"
388 617 args: {
389 618 "repo_name" : "<reponame>",
390 "group_name" : "<groupname>",
391 "perm" : "(None|repository.(read|write|admin))",
392 } No newline at end of file
619 "users_group" : "<usersgroupname>",
620 }
621
622 OUTPUT::
623
624 result: {
625 "msg" : "Revoked perm for group: <usersgroupname> in repo: <reponame>"
626 }
627 error: null No newline at end of file
@@ -1,19 +1,34 b''
1 1 .. _models:
2 2
3 3 The :mod:`models` Module
4 4 ========================
5 5
6 6 .. automodule:: rhodecode.model
7 7 :members:
8 8
9 .. automodule:: rhodecode.model.comment
10 :members:
11
12 .. automodule:: rhodecode.model.notification
13 :members:
14
9 15 .. automodule:: rhodecode.model.permission
10 16 :members:
11
17
18 .. automodule:: rhodecode.model.repo_permission
19 :members:
20
12 21 .. automodule:: rhodecode.model.repo
13 22 :members:
14 23
24 .. automodule:: rhodecode.model.repos_group
25 :members:
26
15 27 .. automodule:: rhodecode.model.scm
16 28 :members:
17
29
18 30 .. automodule:: rhodecode.model.user
19 31 :members:
32
33 .. automodule:: rhodecode.model.users_group
34 :members: No newline at end of file
@@ -1,450 +1,505 b''
1 1 .. _changelog:
2 2
3 3 Changelog
4 4 =========
5 5
6 6
7 1.2.5 (**2012-01-28**)
8 ======================
7 1.3.0 (**2012-02-XX**)
8 ----------------------
9
10 :status: in-progress
11 :branch: beta
9 12
10 13 news
11 ----
14 ++++
15
16 - code review, inspired by github code-comments
17 - #215 rst and markdown README files support
18 - #252 Container-based and proxy pass-through authentication support
19 - #44 branch browser. Filtering of changelog by branches
20 - mercurial bookmarks support
21 - new hover top menu, optimized to add maximum size for important views
22 - configurable clone url template with possibility to specify protocol like
23 ssh:// or http:// and also manually alter other parts of clone_url.
24 - enabled largefiles extension by default
25 - optimized summary file pages and saved a lot of unused space in them
26 - #239 option to manually mark repository as fork
27 - #320 mapping of commit authors to RhodeCode users
28 - #304 hashes are displayed using monospace font
29 - diff configuration, toggle white lines and context lines
30 - #307 configurable diffs, whitespace toggle, increasing context lines
31 - sorting on branches, tags and bookmarks using YUI datatable
32 - improved file filter on files page
33 - implements #330 api method for listing nodes ar particular revision
34 - #73 added linking issues in commit messages to chosen issue tracker url
35 based on user defined regular expression
36 - added linking of changesets in commit messages
37 - new compact changelog with expandable commit messages
38 - firstname and lastname are optional in user creation
39 - #348 added post-create repository hook
40 - #212 global encoding settings is now configurable from .ini files
41 - #227 added repository groups permissions
42 - markdown gets codehilite extensions
43 - new API methods, delete_repositories, grante/revoke permissions for groups
44 and repos
45
46
47 fixes
48 +++++
49
50 - rewrote dbsession management for atomic operations, and better error handling
51 - fixed sorting of repo tables
52 - #326 escape of special html entities in diffs
53 - normalized user_name => username in api attributes
54 - fixes #298 ldap created users with mixed case emails created conflicts
55 on saving a form
56 - fixes issue when owner of a repo couldn't revoke permissions for users
57 and groups
58 - fixes #271 rare JSON serialization problem with statistics
59 - fixes #337 missing validation check for conflicting names of a group with a
60 repositories group
61 - #340 fixed session problem for mysql and celery tasks
62 - fixed #331 RhodeCode mangles repository names if the a repository group
63 contains the "full path" to the repositories
64 - #355 RhodeCode doesn't store encrypted LDAP passwords
65
66 1.2.5 (**2012-01-28**)
67 ----------------------
68
69 news
70 ++++
12 71
13 72 fixes
14 -----
73 +++++
15 74
16 75 - #340 Celery complains about MySQL server gone away, added session cleanup
17 76 for celery tasks
18 77 - #341 "scanning for repositories in None" log message during Rescan was missing
19 78 a parameter
20 79 - fixed creating archives with subrepos. Some hooks were triggered during that
21 80 operation leading to crash.
22 81 - fixed missing email in account page.
23 82 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
24 83 forking on windows impossible
25 84
26 85 1.2.4 (**2012-01-19**)
27 ======================
86 ----------------------
28 87
29 88 news
30 ----
89 ++++
31 90
32 91 - RhodeCode is bundled with mercurial series 2.0.X by default, with
33 92 full support to largefiles extension. Enabled by default in new installations
34 93 - #329 Ability to Add/Remove Groups to/from a Repository via AP
35 94 - added requires.txt file with requirements
36 95
37 96 fixes
38 -----
97 +++++
39 98
40 99 - fixes db session issues with celery when emailing admins
41 100 - #331 RhodeCode mangles repository names if the a repository group
42 101 contains the "full path" to the repositories
43 102 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
44 103 - DB session cleanup after hg protocol operations, fixes issues with
45 104 `mysql has gone away` errors
46 105 - #333 doc fixes for get_repo api function
47 106 - #271 rare JSON serialization problem with statistics enabled
48 107 - #337 Fixes issues with validation of repository name conflicting with
49 108 a group name. A proper message is now displayed.
50 109 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
51 110 doesn't work
52 111 - #316 fixes issues with web description in hgrc files
53 112
54 113 1.2.3 (**2011-11-02**)
55 ======================
114 ----------------------
56 115
57 116 news
58 ----
117 ++++
59 118
60 119 - added option to manage repos group for non admin users
61 120 - added following API methods for get_users, create_user, get_users_groups,
62 121 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
63 122 get_repo, create_repo, add_user_to_repo
64 123 - implements #237 added password confirmation for my account
65 124 and admin edit user.
66 125 - implements #291 email notification for global events are now sent to all
67 126 administrator users, and global config email.
68 127
69 128 fixes
70 -----
129 +++++
71 130
72 131 - added option for passing auth method for smtp mailer
73 132 - #276 issue with adding a single user with id>10 to usergroups
74 133 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
75 134 - #288 fixes managing of repos in a group for non admin user
76 135
77
78 136 1.2.2 (**2011-10-17**)
79 ======================
137 ----------------------
80 138
81 139 news
82 ----
140 ++++
83 141
84 142 - #226 repo groups are available by path instead of numerical id
85 143
86 144 fixes
87 -----
145 +++++
88 146
89 147 - #259 Groups with the same name but with different parent group
90 148 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
91 149 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
92 150 - #265 ldap save fails sometimes on converting attributes to booleans,
93 151 added getter and setter into model that will prevent from this on db model level
94 152 - fixed problems with timestamps issues #251 and #213
95 153 - fixes #266 RhodeCode allows to create repo with the same name and in
96 154 the same parent as group
97 155 - fixes #245 Rescan of the repositories on Windows
98 156 - fixes #248 cannot edit repos inside a group on windows
99 157 - fixes #219 forking problems on windows
100 158
101
102 159 1.2.1 (**2011-10-08**)
103 ======================
160 ----------------------
104 161
105 162 news
106 ----
163 ++++
107 164
108 165
109 166 fixes
110 -----
167 +++++
111 168
112 169 - fixed problems with basic auth and push problems
113 170 - gui fixes
114 171 - fixed logger
115 172
116
117 173 1.2.0 (**2011-10-07**)
118 ======================
174 ----------------------
119 175
120 176 news
121 ----
177 ++++
122 178
123 179 - implemented #47 repository groups
124 180 - implemented #89 Can setup google analytics code from settings menu
125 181 - implemented #91 added nicer looking archive urls with more download options
126 182 like tags, branches
127 183 - implemented #44 into file browsing, and added follow branch option
128 184 - implemented #84 downloads can be enabled/disabled for each repository
129 185 - anonymous repository can be cloned without having to pass default:default
130 186 into clone url
131 187 - fixed #90 whoosh indexer can index chooses repositories passed in command
132 188 line
133 189 - extended journal with day aggregates and paging
134 190 - implemented #107 source code lines highlight ranges
135 191 - implemented #93 customizable changelog on combined revision ranges -
136 192 equivalent of githubs compare view
137 193 - implemented #108 extended and more powerful LDAP configuration
138 194 - implemented #56 users groups
139 195 - major code rewrites optimized codes for speed and memory usage
140 196 - raw and diff downloads are now in git format
141 197 - setup command checks for write access to given path
142 198 - fixed many issues with international characters and unicode. It uses utf8
143 199 decode with replace to provide less errors even with non utf8 encoded strings
144 200 - #125 added API KEY access to feeds
145 201 - #109 Repository can be created from external Mercurial link (aka. remote
146 202 repository, and manually updated (via pull) from admin panel
147 203 - beta git support - push/pull server + basic view for git repos
148 204 - added followers page and forks page
149 205 - server side file creation (with binary file upload interface)
150 206 and edition with commits powered by codemirror
151 207 - #111 file browser file finder, quick lookup files on whole file tree
152 208 - added quick login sliding menu into main page
153 209 - changelog uses lazy loading of affected files details, in some scenarios
154 210 this can improve speed of changelog page dramatically especially for
155 211 larger repositories.
156 212 - implements #214 added support for downloading subrepos in download menu.
157 213 - Added basic API for direct operations on rhodecode via JSON
158 214 - Implemented advanced hook management
159 215
160 216 fixes
161 -----
217 +++++
162 218
163 219 - fixed file browser bug, when switching into given form revision the url was
164 220 not changing
165 221 - fixed propagation to error controller on simplehg and simplegit middlewares
166 222 - fixed error when trying to make a download on empty repository
167 223 - fixed problem with '[' chars in commit messages in journal
168 224 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
169 225 - journal fork fixes
170 226 - removed issue with space inside renamed repository after deletion
171 227 - fixed strange issue on formencode imports
172 228 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
173 229 - #150 fixes for errors on repositories mapped in db but corrupted in
174 230 filesystem
175 231 - fixed problem with ascendant characters in realm #181
176 232 - fixed problem with sqlite file based database connection pool
177 233 - whoosh indexer and code stats share the same dynamic extensions map
178 234 - fixes #188 - relationship delete of repo_to_perm entry on user removal
179 235 - fixes issue #189 Trending source files shows "show more" when no more exist
180 236 - fixes issue #197 Relative paths for pidlocks
181 237 - fixes issue #198 password will require only 3 chars now for login form
182 238 - fixes issue #199 wrong redirection for non admin users after creating a repository
183 239 - fixes issues #202, bad db constraint made impossible to attach same group
184 240 more than one time. Affects only mysql/postgres
185 241 - fixes #218 os.kill patch for windows was missing sig param
186 242 - improved rendering of dag (they are not trimmed anymore when number of
187 243 heads exceeds 5)
188
189
244
190 245 1.1.8 (**2011-04-12**)
191 ======================
246 ----------------------
192 247
193 248 news
194 ----
249 ++++
195 250
196 251 - improved windows support
197 252
198 253 fixes
199 -----
254 +++++
200 255
201 256 - fixed #140 freeze of python dateutil library, since new version is python2.x
202 257 incompatible
203 258 - setup-app will check for write permission in given path
204 259 - cleaned up license info issue #149
205 260 - fixes for issues #137,#116 and problems with unicode and accented characters.
206 261 - fixes crashes on gravatar, when passed in email as unicode
207 262 - fixed tooltip flickering problems
208 263 - fixed came_from redirection on windows
209 264 - fixed logging modules, and sql formatters
210 265 - windows fixes for os.kill issue #133
211 266 - fixes path splitting for windows issues #148
212 267 - fixed issue #143 wrong import on migration to 1.1.X
213 268 - fixed problems with displaying binary files, thanks to Thomas Waldmann
214 269 - removed name from archive files since it's breaking ui for long repo names
215 270 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
216 271 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
217 272 Thomas Waldmann
218 273 - fixed issue #166 summary pager was skipping 10 revisions on second page
219 274
220 275
221 276 1.1.7 (**2011-03-23**)
222 ======================
277 ----------------------
223 278
224 279 news
225 ----
280 ++++
226 281
227 282 fixes
228 -----
283 +++++
229 284
230 285 - fixed (again) #136 installation support for FreeBSD
231 286
232 287
233 288 1.1.6 (**2011-03-21**)
234 ======================
289 ----------------------
235 290
236 291 news
237 ----
292 ++++
238 293
239 294 fixes
240 -----
295 +++++
241 296
242 297 - fixed #136 installation support for FreeBSD
243 298 - RhodeCode will check for python version during installation
244 299
245 300 1.1.5 (**2011-03-17**)
246 ======================
301 ----------------------
247 302
248 303 news
249 ----
304 ++++
250 305
251 306 - basic windows support, by exchanging pybcrypt into sha256 for windows only
252 307 highly inspired by idea of mantis406
253 308
254 309 fixes
255 -----
310 +++++
256 311
257 312 - fixed sorting by author in main page
258 313 - fixed crashes with diffs on binary files
259 314 - fixed #131 problem with boolean values for LDAP
260 315 - fixed #122 mysql problems thanks to striker69
261 316 - fixed problem with errors on calling raw/raw_files/annotate functions
262 317 with unknown revisions
263 318 - fixed returned rawfiles attachment names with international character
264 319 - cleaned out docs, big thanks to Jason Harris
265 320
266 321 1.1.4 (**2011-02-19**)
267 ======================
322 ----------------------
268 323
269 324 news
270 ----
325 ++++
271 326
272 327 fixes
273 -----
328 +++++
274 329
275 330 - fixed formencode import problem on settings page, that caused server crash
276 331 when that page was accessed as first after server start
277 332 - journal fixes
278 333 - fixed option to access repository just by entering http://server/<repo_name>
279 334
280 335 1.1.3 (**2011-02-16**)
281 ======================
336 ----------------------
282 337
283 338 news
284 ----
339 ++++
285 340
286 341 - implemented #102 allowing the '.' character in username
287 342 - added option to access repository just by entering http://server/<repo_name>
288 343 - celery task ignores result for better performance
289 344
290 345 fixes
291 -----
346 +++++
292 347
293 348 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
294 349 apollo13 and Johan Walles
295 350 - small fixes in journal
296 351 - fixed problems with getting setting for celery from .ini files
297 352 - registration, password reset and login boxes share the same title as main
298 353 application now
299 354 - fixed #113: to high permissions to fork repository
300 355 - fixed problem with '[' chars in commit messages in journal
301 356 - removed issue with space inside renamed repository after deletion
302 357 - db transaction fixes when filesystem repository creation failed
303 358 - fixed #106 relation issues on databases different than sqlite
304 359 - fixed static files paths links to use of url() method
305 360
306 361 1.1.2 (**2011-01-12**)
307 ======================
362 ----------------------
308 363
309 364 news
310 ----
365 ++++
311 366
312 367
313 368 fixes
314 -----
369 +++++
315 370
316 371 - fixes #98 protection against float division of percentage stats
317 372 - fixed graph bug
318 373 - forced webhelpers version since it was making troubles during installation
319 374
320 375 1.1.1 (**2011-01-06**)
321 ======================
376 ----------------------
322 377
323 378 news
324 ----
379 ++++
325 380
326 381 - added force https option into ini files for easier https usage (no need to
327 382 set server headers with this options)
328 383 - small css updates
329 384
330 385 fixes
331 -----
386 +++++
332 387
333 388 - fixed #96 redirect loop on files view on repositories without changesets
334 389 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
335 390 and server crashed with errors
336 391 - fixed large tooltips problems on main page
337 392 - fixed #92 whoosh indexer is more error proof
338 393
339 394 1.1.0 (**2010-12-18**)
340 ======================
395 ----------------------
341 396
342 397 news
343 ----
398 ++++
344 399
345 400 - rewrite of internals for vcs >=0.1.10
346 401 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
347 402 with older clients
348 403 - anonymous access, authentication via ldap
349 - performance upgrade for cached repos list - each repository has it's own
404 - performance upgrade for cached repos list - each repository has its own
350 405 cache that's invalidated when needed.
351 406 - performance upgrades on repositories with large amount of commits (20K+)
352 407 - main page quick filter for filtering repositories
353 408 - user dashboards with ability to follow chosen repositories actions
354 409 - sends email to admin on new user registration
355 410 - added cache/statistics reset options into repository settings
356 411 - more detailed action logger (based on hooks) with pushed changesets lists
357 412 and options to disable those hooks from admin panel
358 413 - introduced new enhanced changelog for merges that shows more accurate results
359 414 - new improved and faster code stats (based on pygments lexers mapping tables,
360 415 showing up to 10 trending sources for each repository. Additionally stats
361 416 can be disabled in repository settings.
362 417 - gui optimizations, fixed application width to 1024px
363 418 - added cut off (for large files/changesets) limit into config files
364 419 - whoosh, celeryd, upgrade moved to paster command
365 420 - other than sqlite database backends can be used
366 421
367 422 fixes
368 -----
423 +++++
369 424
370 425 - fixes #61 forked repo was showing only after cache expired
371 426 - fixes #76 no confirmation on user deletes
372 427 - fixes #66 Name field misspelled
373 428 - fixes #72 block user removal when he owns repositories
374 429 - fixes #69 added password confirmation fields
375 430 - fixes #87 RhodeCode crashes occasionally on updating repository owner
376 431 - fixes #82 broken annotations on files with more than 1 blank line at the end
377 432 - a lot of fixes and tweaks for file browser
378 433 - fixed detached session issues
379 434 - fixed when user had no repos he would see all repos listed in my account
380 435 - fixed ui() instance bug when global hgrc settings was loaded for server
381 436 instance and all hgrc options were merged with our db ui() object
382 437 - numerous small bugfixes
383 438
384 439 (special thanks for TkSoh for detailed feedback)
385 440
386 441
387 442 1.0.2 (**2010-11-12**)
388 ======================
443 ----------------------
389 444
390 445 news
391 ----
446 ++++
392 447
393 448 - tested under python2.7
394 449 - bumped sqlalchemy and celery versions
395 450
396 451 fixes
397 -----
452 +++++
398 453
399 454 - fixed #59 missing graph.js
400 455 - fixed repo_size crash when repository had broken symlinks
401 456 - fixed python2.5 crashes.
402 457
403 458
404 459 1.0.1 (**2010-11-10**)
405 ======================
460 ----------------------
406 461
407 462 news
408 ----
463 ++++
409 464
410 465 - small css updated
411 466
412 467 fixes
413 -----
468 +++++
414 469
415 470 - fixed #53 python2.5 incompatible enumerate calls
416 471 - fixed #52 disable mercurial extension for web
417 472 - fixed #51 deleting repositories don't delete it's dependent objects
418 473
419 474
420 475 1.0.0 (**2010-11-02**)
421 ======================
476 ----------------------
422 477
423 478 - security bugfix simplehg wasn't checking for permissions on commands
424 479 other than pull or push.
425 480 - fixed doubled messages after push or pull in admin journal
426 481 - templating and css corrections, fixed repo switcher on chrome, updated titles
427 482 - admin menu accessible from options menu on repository view
428 483 - permissions cached queries
429 484
430 485 1.0.0rc4 (**2010-10-12**)
431 ==========================
486 --------------------------
432 487
433 488 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
434 489 - removed cache_manager settings from sqlalchemy meta
435 490 - added sqlalchemy cache settings to ini files
436 491 - validated password length and added second try of failure on paster setup-app
437 492 - fixed setup database destroy prompt even when there was no db
438 493
439 494
440 495 1.0.0rc3 (**2010-10-11**)
441 =========================
496 -------------------------
442 497
443 498 - fixed i18n during installation.
444 499
445 500 1.0.0rc2 (**2010-10-11**)
446 =========================
501 -------------------------
447 502
448 503 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
449 504 occure. After vcs is fixed it'll be put back again.
450 505 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,59 +1,59 b''
1 1 .. _index:
2 2
3 3 .. include:: ./../README.rst
4 4
5 Documentation
6 -------------
5 Users Guide
6 -----------
7 7
8 8 **Installation:**
9 9
10 10 .. toctree::
11 11 :maxdepth: 1
12 12
13 13 installation
14 14 setup
15 15 upgrade
16 16
17 17 **Usage**
18 18
19 19 .. toctree::
20 20 :maxdepth: 1
21 21
22 22 usage/general
23 usage/enable_git
23 usage/git_support
24 24 usage/statistics
25 25 usage/backup
26 usage/api_key_access
27 26
28 27 **Develop**
29 28
30 29 .. toctree::
31 30 :maxdepth: 1
32 31
33 32 contributing
34 33 changelog
35 34
36 35 **API**
37 36
38 37 .. toctree::
39 :maxdepth: 2
38 :maxdepth: 1
40 39
41 api/index
40 api/api
41 api/models
42 42
43 43
44 44 Other topics
45 45 ------------
46 46
47 47 * :ref:`genindex`
48 48 * :ref:`search`
49 49
50 50 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
51 51 .. _python: http://www.python.org/
52 52 .. _django: http://www.djangoproject.com/
53 53 .. _mercurial: http://mercurial.selenic.com/
54 54 .. _bitbucket: http://bitbucket.org/
55 55 .. _subversion: http://subversion.tigris.org/
56 56 .. _git: http://git-scm.com/
57 57 .. _celery: http://celeryproject.org/
58 58 .. _Sphinx: http://sphinx.pocoo.org/
59 59 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,605 +1,725 b''
1 1 .. _setup:
2 2
3 3 Setup
4 4 =====
5 5
6 6
7 7 Setting up RhodeCode
8 8 --------------------
9 9
10 10 First, you will need to create a RhodeCode configuration file. Run the
11 11 following command to do this::
12 12
13 13 paster make-config RhodeCode production.ini
14 14
15 15 - This will create the file `production.ini` in the current directory. This
16 16 configuration file contains the various settings for RhodeCode, e.g proxy
17 17 port, email settings, usage of static files, cache, celery settings and
18 18 logging.
19 19
20 20
21 21 Next, you need to create the databases used by RhodeCode. I recommend that you
22 22 use sqlite (default) or postgresql. If you choose a database other than the
23 23 default ensure you properly adjust the db url in your production.ini
24 24 configuration file to use this other database. Create the databases by running
25 25 the following command::
26 26
27 27 paster setup-app production.ini
28 28
29 29 This will prompt you for a "root" path. This "root" path is the location where
30 30 RhodeCode will store all of its repositories on the current machine. After
31 31 entering this "root" path ``setup-app`` will also prompt you for a username
32 32 and password for the initial admin account which ``setup-app`` sets up for you.
33 33
34 34 - The ``setup-app`` command will create all of the needed tables and an admin
35 35 account. When choosing a root path you can either use a new empty location,
36 36 or a location which already contains existing repositories. If you choose a
37 37 location which contains existing repositories RhodeCode will simply add all
38 38 of the repositories at the chosen location to it's database. (Note: make
39 39 sure you specify the correct path to the root).
40 40 - Note: the given path for mercurial_ repositories **must** be write accessible
41 41 for the application. It's very important since the RhodeCode web interface
42 42 will work without write access, but when trying to do a push it will
43 43 eventually fail with permission denied errors unless it has write access.
44 44
45 45 You are now ready to use RhodeCode, to run it simply execute::
46 46
47 47 paster serve production.ini
48 48
49 49 - This command runs the RhodeCode server. The web app should be available at the
50 50 127.0.0.1:5000. This ip and port is configurable via the production.ini
51 51 file created in previous step
52 52 - Use the admin account you created above when running ``setup-app`` to login
53 53 to the web app.
54 54 - The default permissions on each repository is read, and the owner is admin.
55 55 Remember to update these if needed.
56 56 - In the admin panel you can toggle ldap, anonymous, permissions settings. As
57 57 well as edit more advanced options on users and repositories
58 58
59 59 Try copying your own mercurial repository into the "root" directory you are
60 60 using, then from within the RhodeCode web application choose Admin >
61 61 repositories. Then choose Add New Repository. Add the repository you copied
62 62 into the root. Test that you can browse your repository from within RhodeCode
63 63 and then try cloning your repository from RhodeCode with::
64 64
65 65 hg clone http://127.0.0.1:5000/<repository name>
66 66
67 67 where *repository name* is replaced by the name of your repository.
68 68
69 69 Using RhodeCode with SSH
70 70 ------------------------
71 71
72 72 RhodeCode currently only hosts repositories using http and https. (The addition
73 73 of ssh hosting is a planned future feature.) However you can easily use ssh in
74 74 parallel with RhodeCode. (Repository access via ssh is a standard "out of
75 75 the box" feature of mercurial_ and you can use this to access any of the
76 76 repositories that RhodeCode is hosting. See PublishingRepositories_)
77 77
78 78 RhodeCode repository structures are kept in directories with the same name
79 79 as the project. When using repository groups, each group is a subdirectory.
80 80 This allows you to easily use ssh for accessing repositories.
81 81
82 82 In order to use ssh you need to make sure that your web-server and the users
83 83 login accounts have the correct permissions set on the appropriate directories.
84 84 (Note that these permissions are independent of any permissions you have set up
85 85 using the RhodeCode web interface.)
86 86
87 87 If your main directory (the same as set in RhodeCode settings) is for example
88 88 set to **/home/hg** and the repository you are using is named `rhodecode`, then
89 89 to clone via ssh you should run::
90 90
91 91 hg clone ssh://user@server.com/home/hg/rhodecode
92 92
93 93 Using other external tools such as mercurial-server_ or using ssh key based
94 94 authentication is fully supported.
95 95
96 96 Note: In an advanced setup, in order for your ssh access to use the same
97 97 permissions as set up via the RhodeCode web interface, you can create an
98 98 authentication hook to connect to the rhodecode db and runs check functions for
99 99 permissions against that.
100 100
101 101 Setting up Whoosh full text search
102 102 ----------------------------------
103 103
104 104 Starting from version 1.1 the whoosh index can be build by using the paster
105 105 command ``make-index``. To use ``make-index`` you must specify the configuration
106 106 file that stores the location of the index. You may specify the location of the
107 107 repositories (`--repo-location`). If not specified, this value is retrieved
108 108 from the RhodeCode database. This was required prior to 1.2. Starting from
109 109 version 1.2 it is also possible to specify a comma separated list of
110 110 repositories (`--index-only`) to build index only on chooses repositories
111 111 skipping any other found in repos location
112 112
113 113 You may optionally pass the option `-f` to enable a full index rebuild. Without
114 114 the `-f` option, indexing will run always in "incremental" mode.
115 115
116 116 For an incremental index build use::
117 117
118 118 paster make-index production.ini
119 119
120 120 For a full index rebuild use::
121 121
122 122 paster make-index production.ini -f
123 123
124 124
125 125 building index just for chosen repositories is possible with such command::
126 126
127 127 paster make-index production.ini --index-only=vcs,rhodecode
128 128
129 129
130 130 In order to do periodical index builds and keep your index always up to date.
131 131 It's recommended to do a crontab entry for incremental indexing.
132 132 An example entry might look like this::
133 133
134 134 /path/to/python/bin/paster make-index /path/to/rhodecode/production.ini
135 135
136 136 When using incremental mode (the default) whoosh will check the last
137 137 modification date of each file and add it to be reindexed if a newer file is
138 138 available. The indexing daemon checks for any removed files and removes them
139 139 from index.
140 140
141 141 If you want to rebuild index from scratch, you can use the `-f` flag as above,
142 142 or in the admin panel you can check `build from scratch` flag.
143 143
144 144
145 145 Setting up LDAP support
146 146 -----------------------
147 147
148 148 RhodeCode starting from version 1.1 supports ldap authentication. In order
149 149 to use LDAP, you have to install the python-ldap_ package. This package is
150 150 available via pypi, so you can install it by running
151 151
152 152 using easy_install::
153 153
154 154 easy_install python-ldap
155 155
156 156 using pip::
157 157
158 158 pip install python-ldap
159 159
160 160 .. note::
161 161 python-ldap requires some certain libs on your system, so before installing
162 162 it check that you have at least `openldap`, and `sasl` libraries.
163 163
164 164 LDAP settings are located in admin->ldap section,
165 165
166 166 Here's a typical ldap setup::
167 167
168 168 Connection settings
169 169 Enable LDAP = checked
170 170 Host = host.example.org
171 171 Port = 389
172 172 Account = <account>
173 173 Password = <password>
174 174 Connection Security = LDAPS connection
175 175 Certificate Checks = DEMAND
176 176
177 177 Search settings
178 178 Base DN = CN=users,DC=host,DC=example,DC=org
179 179 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
180 180 LDAP Search Scope = SUBTREE
181 181
182 182 Attribute mappings
183 183 Login Attribute = uid
184 184 First Name Attribute = firstName
185 185 Last Name Attribute = lastName
186 186 E-mail Attribute = mail
187 187
188 188 .. _enable_ldap:
189 189
190 190 Enable LDAP : required
191 191 Whether to use LDAP for authenticating users.
192 192
193 193 .. _ldap_host:
194 194
195 195 Host : required
196 196 LDAP server hostname or IP address.
197 197
198 198 .. _Port:
199 199
200 200 Port : required
201 201 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
202 202
203 203 .. _ldap_account:
204 204
205 205 Account : optional
206 206 Only required if the LDAP server does not allow anonymous browsing of
207 207 records. This should be a special account for record browsing. This
208 208 will require `LDAP Password`_ below.
209 209
210 210 .. _LDAP Password:
211 211
212 212 Password : optional
213 213 Only required if the LDAP server does not allow anonymous browsing of
214 214 records.
215 215
216 216 .. _Enable LDAPS:
217 217
218 218 Connection Security : required
219 219 Defines the connection to LDAP server
220 220
221 221 No encryption
222 222 Plain non encrypted connection
223 223
224 224 LDAPS connection
225 225 Enable ldaps connection. It will likely require `Port`_ to be set to
226 226 a different value (standard LDAPS port is 636). When LDAPS is enabled
227 227 then `Certificate Checks`_ is required.
228 228
229 229 START_TLS on LDAP connection
230 230 START TLS connection
231 231
232 232 .. _Certificate Checks:
233 233
234 234 Certificate Checks : optional
235 235 How SSL certificates verification is handled - this is only useful when
236 236 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
237 237 while the other options are susceptible to man-in-the-middle attacks. SSL
238 238 certificates can be installed to /etc/openldap/cacerts so that the
239 239 DEMAND or HARD options can be used with self-signed certificates or
240 240 certificates that do not have traceable certificates of authority.
241 241
242 242 NEVER
243 243 A serve certificate will never be requested or checked.
244 244
245 245 ALLOW
246 246 A server certificate is requested. Failure to provide a
247 247 certificate or providing a bad certificate will not terminate the
248 248 session.
249 249
250 250 TRY
251 251 A server certificate is requested. Failure to provide a
252 252 certificate does not halt the session; providing a bad certificate
253 253 halts the session.
254 254
255 255 DEMAND
256 256 A server certificate is requested and must be provided and
257 257 authenticated for the session to proceed.
258 258
259 259 HARD
260 260 The same as DEMAND.
261 261
262 262 .. _Base DN:
263 263
264 264 Base DN : required
265 265 The Distinguished Name (DN) where searches for users will be performed.
266 266 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
267 267
268 268 .. _LDAP Filter:
269 269
270 270 LDAP Filter : optional
271 271 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
272 272 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
273 273 which LDAP objects are identified as representing Users for
274 274 authentication. The filter is augmented by `Login Attribute`_ below.
275 275 This can commonly be left blank.
276 276
277 277 .. _LDAP Search Scope:
278 278
279 279 LDAP Search Scope : required
280 280 This limits how far LDAP will search for a matching object.
281 281
282 282 BASE
283 283 Only allows searching of `Base DN`_ and is usually not what you
284 284 want.
285 285
286 286 ONELEVEL
287 287 Searches all entries under `Base DN`_, but not Base DN itself.
288 288
289 289 SUBTREE
290 290 Searches all entries below `Base DN`_, but not Base DN itself.
291 291 When using SUBTREE `LDAP Filter`_ is useful to limit object
292 292 location.
293 293
294 294 .. _Login Attribute:
295 295
296 296 Login Attribute : required
297 297 The LDAP record attribute that will be matched as the USERNAME or
298 298 ACCOUNT used to connect to RhodeCode. This will be added to `LDAP
299 299 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
300 300 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
301 301 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
302 302 ::
303 303
304 304 (&(LDAPFILTER)(uid=jsmith))
305 305
306 306 .. _ldap_attr_firstname:
307 307
308 308 First Name Attribute : required
309 309 The LDAP record attribute which represents the user's first name.
310 310
311 311 .. _ldap_attr_lastname:
312 312
313 313 Last Name Attribute : required
314 314 The LDAP record attribute which represents the user's last name.
315 315
316 316 .. _ldap_attr_email:
317 317
318 318 Email Attribute : required
319 319 The LDAP record attribute which represents the user's email address.
320 320
321 321 If all data are entered correctly, and python-ldap_ is properly installed
322 322 users should be granted access to RhodeCode with ldap accounts. At this
323 323 time user information is copied from LDAP into the RhodeCode user database.
324 324 This means that updates of an LDAP user object may not be reflected as a
325 325 user update in RhodeCode.
326 326
327 327 If You have problems with LDAP access and believe You entered correct
328 328 information check out the RhodeCode logs, any error messages sent from LDAP
329 329 will be saved there.
330 330
331 331 Active Directory
332 332 ''''''''''''''''
333 333
334 334 RhodeCode can use Microsoft Active Directory for user authentication. This
335 335 is done through an LDAP or LDAPS connection to Active Directory. The
336 336 following LDAP configuration settings are typical for using Active
337 337 Directory ::
338 338
339 339 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
340 340 Login Attribute = sAMAccountName
341 341 First Name Attribute = givenName
342 342 Last Name Attribute = sn
343 343 E-mail Attribute = mail
344 344
345 345 All other LDAP settings will likely be site-specific and should be
346 346 appropriately configured.
347 347
348 348
349 Authentication by container or reverse-proxy
350 --------------------------------------------
351
352 Starting with version 1.3, RhodeCode supports delegating the authentication
353 of users to its WSGI container, or to a reverse-proxy server through which all
354 clients access the application.
355
356 When these authentication methods are enabled in RhodeCode, it uses the
357 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
358 perform the authentication itself. The authorization, however, is still done by
359 RhodeCode according to its settings.
360
361 When a user logs in for the first time using these authentication methods,
362 a matching user account is created in RhodeCode with default permissions. An
363 administrator can then modify it using RhodeCode's admin interface.
364 It's also possible for an administrator to create accounts and configure their
365 permissions before the user logs in for the first time.
366
367 Container-based authentication
368 ''''''''''''''''''''''''''''''
369
370 In a container-based authentication setup, RhodeCode reads the user name from
371 the ``REMOTE_USER`` server variable provided by the WSGI container.
372
373 After setting up your container (see `Apache's WSGI config`_), you'd need
374 to configure it to require authentication on the location configured for
375 RhodeCode.
376
377 In order for RhodeCode to start using the provided username, you should set the
378 following in the [app:main] section of your .ini file::
379
380 container_auth_enabled = true
381
382
383 Proxy pass-through authentication
384 '''''''''''''''''''''''''''''''''
385
386 In a proxy pass-through authentication setup, RhodeCode reads the user name
387 from the ``X-Forwarded-User`` request header, which should be configured to be
388 sent by the reverse-proxy server.
389
390 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
391 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
392 configure the authentication and add the username in a request header named
393 ``X-Forwarded-User``.
394
395 For example, the following config section for Apache sets a subdirectory in a
396 reverse-proxy setup with basic auth::
397
398 <Location /<someprefix> >
399 ProxyPass http://127.0.0.1:5000/<someprefix>
400 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
401 SetEnvIf X-Url-Scheme https HTTPS=1
402
403 AuthType Basic
404 AuthName "RhodeCode authentication"
405 AuthUserFile /home/web/rhodecode/.htpasswd
406 require valid-user
407
408 RequestHeader unset X-Forwarded-User
409
410 RewriteEngine On
411 RewriteCond %{LA-U:REMOTE_USER} (.+)
412 RewriteRule .* - [E=RU:%1]
413 RequestHeader set X-Forwarded-User %{RU}e
414 </Location>
415
416 In order for RhodeCode to start using the forwarded username, you should set
417 the following in the [app:main] section of your .ini file::
418
419 proxypass_auth_enabled = true
420
421 .. note::
422 If you enable proxy pass-through authentication, make sure your server is
423 only accessible through the proxy. Otherwise, any client would be able to
424 forge the authentication header and could effectively become authenticated
425 using any account of their liking.
426
427 Integration with Issue trackers
428 -------------------------------
429
430 RhodeCode provides a simple integration with issue trackers. It's possible
431 to define a regular expression that will fetch issue id stored in commit
432 messages and replace that with an url to this issue. To enable this simply
433 uncomment following variables in the ini file::
434
435 url_pat = (?:^#|\s#)(\w+)
436 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
437 issue_prefix = #
438
439 `url_pat` is the regular expression that will fetch issues from commit messages.
440 Default regex will match issues in format of #<number> eg. #300.
441
442 Matched issues will be replace with the link specified as `issue_server_link`
443 {id} will be replaced with issue id, and {repo} with repository name.
444 Since the # is striped `issue_prefix` is added as a prefix to url.
445 `issue_prefix` can be something different than # if you pass
446 ISSUE- as issue prefix this will generate an url in format::
447
448 <a href="https://myissueserver.com/example_repo/issue/300">ISSUE-300</a>
349 449
350 450 Hook management
351 451 ---------------
352 452
353 453 Hooks can be managed in similar way to this used in .hgrc files.
354 454 To access hooks setting click `advanced setup` on Hooks section of Mercurial
355 455 Settings in Admin.
356 456
357 457 There are 4 built in hooks that cannot be changed (only enable/disable by
358 458 checkboxes on previos section).
359 459 To add another custom hook simply fill in first section with
360 460 <name>.<hook_type> and the second one with hook path. Example hooks
361 461 can be found at *rhodecode.lib.hooks*.
362 462
363 463
464 Changing default encoding
465 -------------------------
466
467 By default RhodeCode uses utf8 encoding, starting from 1.3 series this
468 can be changed, simply edit default_encoding in .ini file to desired one.
469 This affects many parts in rhodecode including commiters names, filenames,
470 encoding of commit messages. In addition RhodeCode can detect if `chardet`
471 library is installed. If `chardet` is detected RhodeCode will fallback to it
472 when there are encode/decode errors.
473
474
364 475 Setting Up Celery
365 476 -----------------
366 477
367 478 Since version 1.1 celery is configured by the rhodecode ini configuration files.
368 479 Simply set use_celery=true in the ini file then add / change the configuration
369 480 variables inside the ini file.
370 481
371 482 Remember that the ini files use the format with '.' not with '_' like celery.
372 483 So for example setting `BROKER_HOST` in celery means setting `broker.host` in
373 484 the config file.
374 485
375 486 In order to start using celery run::
376 487
377 488 paster celeryd <configfile.ini>
378 489
379 490
380 491 .. note::
381 492 Make sure you run this command from the same virtualenv, and with the same
382 493 user that rhodecode runs.
383 494
384 495 HTTPS support
385 496 -------------
386 497
387 498 There are two ways to enable https:
388 499
389 500 - Set HTTP_X_URL_SCHEME in your http server headers, than rhodecode will
390 501 recognize this headers and make proper https redirections
391 502 - Alternatively, change the `force_https = true` flag in the ini configuration
392 503 to force using https, no headers are needed than to enable https
393 504
394 505
395 506 Nginx virtual host example
396 507 --------------------------
397 508
398 509 Sample config for nginx using proxy::
399 510
511 upstream rc {
512 server 127.0.0.1:5000;
513 # add more instances for load balancing
514 #server 127.0.0.1:5001;
515 #server 127.0.0.1:5002;
516 }
517
400 518 server {
401 519 listen 80;
402 520 server_name hg.myserver.com;
403 521 access_log /var/log/nginx/rhodecode.access.log;
404 522 error_log /var/log/nginx/rhodecode.error.log;
523
405 524 location / {
406 root /var/www/rhodecode/rhodecode/public/;
407 if (!-f $request_filename){
408 proxy_pass http://127.0.0.1:5000;
409 }
410 #this is important if you want to use https !!!
411 proxy_set_header X-Url-Scheme $scheme;
412 include /etc/nginx/proxy.conf;
525 try_files $uri @rhode;
413 526 }
527
528 location @rhode {
529 proxy_pass http://rc;
530 include /etc/nginx/proxy.conf;
531 }
532
414 533 }
415 534
416 535 Here's the proxy.conf. It's tuned so it will not timeout on long
417 536 pushes or large pushes::
418
537
419 538 proxy_redirect off;
420 539 proxy_set_header Host $host;
540 proxy_set_header X-Url-Scheme $scheme;
421 541 proxy_set_header X-Host $http_host;
422 542 proxy_set_header X-Real-IP $remote_addr;
423 543 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
424 544 proxy_set_header Proxy-host $proxy_host;
425 545 client_max_body_size 400m;
426 546 client_body_buffer_size 128k;
427 547 proxy_buffering off;
428 548 proxy_connect_timeout 7200;
429 549 proxy_send_timeout 7200;
430 550 proxy_read_timeout 7200;
431 551 proxy_buffers 8 32k;
432 552
433 553 Also, when using root path with nginx you might set the static files to false
434 554 in the production.ini file::
435 555
436 556 [app:main]
437 557 use = egg:rhodecode
438 558 full_stack = true
439 559 static_files = false
440 560 lang=en
441 561 cache_dir = %(here)s/data
442 562
443 563 In order to not have the statics served by the application. This improves speed.
444 564
445 565
446 566 Apache virtual host reverse proxy example
447 567 -----------------------------------------
448 568
449 569 Here is a sample configuration file for apache using proxy::
450 570
451 571 <VirtualHost *:80>
452 572 ServerName hg.myserver.com
453 573 ServerAlias hg.myserver.com
454 574
455 575 <Proxy *>
456 576 Order allow,deny
457 577 Allow from all
458 578 </Proxy>
459 579
460 580 #important !
461 581 #Directive to properly generate url (clone url) for pylons
462 582 ProxyPreserveHost On
463 583
464 584 #rhodecode instance
465 585 ProxyPass / http://127.0.0.1:5000/
466 586 ProxyPassReverse / http://127.0.0.1:5000/
467 587
468 588 #to enable https use line below
469 589 #SetEnvIf X-Url-Scheme https HTTPS=1
470 590
471 591 </VirtualHost>
472 592
473 593
474 594 Additional tutorial
475 595 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
476 596
477 597
478 598 Apache as subdirectory
479 599 ----------------------
480 600
481 601 Apache subdirectory part::
482 602
483 603 <Location /<someprefix> >
484 604 ProxyPass http://127.0.0.1:5000/<someprefix>
485 605 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
486 606 SetEnvIf X-Url-Scheme https HTTPS=1
487 607 </Location>
488 608
489 609 Besides the regular apache setup you will need to add the following line
490 610 into [app:main] section of your .ini file::
491 611
492 612 filter-with = proxy-prefix
493 613
494 614 Add the following at the end of the .ini file::
495 615
496 616 [filter:proxy-prefix]
497 617 use = egg:PasteDeploy#prefix
498 618 prefix = /<someprefix>
499 619
500 620
501 621 then change <someprefix> into your choosen prefix
502 622
503 623 Apache's WSGI config
504 624 --------------------
505 625
506 626 Alternatively, RhodeCode can be set up with Apache under mod_wsgi. For
507 627 that, you'll need to:
508 628
509 629 - Install mod_wsgi. If using a Debian-based distro, you can install
510 630 the package libapache2-mod-wsgi::
511 631
512 632 aptitude install libapache2-mod-wsgi
513 633
514 634 - Enable mod_wsgi::
515 635
516 636 a2enmod wsgi
517 637
518 638 - Create a wsgi dispatch script, like the one below. Make sure you
519 639 check the paths correctly point to where you installed RhodeCode
520 640 and its Python Virtual Environment.
521 641 - Enable the WSGIScriptAlias directive for the wsgi dispatch script,
522 642 as in the following example. Once again, check the paths are
523 643 correctly specified.
524 644
525 645 Here is a sample excerpt from an Apache Virtual Host configuration file::
526 646
527 647 WSGIDaemonProcess pylons user=www-data group=www-data processes=1 \
528 648 threads=4 \
529 649 python-path=/home/web/rhodecode/pyenv/lib/python2.6/site-packages
530 650 WSGIScriptAlias / /home/web/rhodecode/dispatch.wsgi
531 651
532 652 Example wsgi dispatch script::
533 653
534 654 import os
535 655 os.environ["HGENCODING"] = "UTF-8"
536 656 os.environ['PYTHON_EGG_CACHE'] = '/home/web/rhodecode/.egg-cache'
537 657
538 658 # sometimes it's needed to set the curent dir
539 659 os.chdir('/home/web/rhodecode/')
540 660
541 661 import site
542 662 site.addsitedir("/home/web/rhodecode/pyenv/lib/python2.6/site-packages")
543 663
544 664 from paste.deploy import loadapp
545 665 from paste.script.util.logging_config import fileConfig
546 666
547 667 fileConfig('/home/web/rhodecode/production.ini')
548 668 application = loadapp('config:/home/web/rhodecode/production.ini')
549 669
550 670 Note: when using mod_wsgi you'll need to install the same version of
551 671 Mercurial that's inside RhodeCode's virtualenv also on the system's Python
552 672 environment.
553 673
554 674
555 675 Other configuration files
556 676 -------------------------
557 677
558 678 Some example init.d scripts can be found here, for debian and gentoo:
559 679
560 680 https://rhodecode.org/rhodecode/files/tip/init.d
561 681
562 682
563 683 Troubleshooting
564 684 ---------------
565 685
566 686 :Q: **Missing static files?**
567 687 :A: Make sure either to set the `static_files = true` in the .ini file or
568 688 double check the root path for your http setup. It should point to
569 689 for example:
570 690 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
571 691
572 692 |
573 693
574 694 :Q: **Can't install celery/rabbitmq**
575 695 :A: Don't worry RhodeCode works without them too. No extra setup is required.
576 696
577 697 |
578 698
579 699 :Q: **Long lasting push timeouts?**
580 700 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
581 701 are caused by https server and not RhodeCode.
582 702
583 703 |
584 704
585 705 :Q: **Large pushes timeouts?**
586 706 :A: Make sure you set a proper max_body_size for the http server.
587 707
588 708 |
589 709
590 710 :Q: **Apache doesn't pass basicAuth on pull/push?**
591 711 :A: Make sure you added `WSGIPassAuthorization true`.
592 712
593 713 For further questions search the `Issues tracker`_, or post a message in the
594 714 `google group rhodecode`_
595 715
596 716 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
597 717 .. _python: http://www.python.org/
598 718 .. _mercurial: http://mercurial.selenic.com/
599 719 .. _celery: http://celeryproject.org/
600 720 .. _rabbitmq: http://www.rabbitmq.com/
601 721 .. _python-ldap: http://www.python-ldap.org/
602 722 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
603 723 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
604 724 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
605 725 .. _google group rhodecode: http://groups.google.com/group/rhodecode
@@ -1,18 +1,18 b''
1 1 {% extends "basic/layout.html" %}
2 2
3 3 {% block sidebarlogo %}
4 <h3>Support my development effort.</h3>
4 <h3>Support RhodeCode development.</h3>
5 5 <div style="text-align:center">
6 6 <form action="https://www.paypal.com/cgi-bin/webscr" method="post">
7 7 <input type="hidden" name="cmd" value="_s-xclick">
8 8 <input type="hidden" name="hosted_button_id" value="8U2LLRPLBKWDU">
9 <input style="border:0px !important" type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif"
9 <input style="border:0px !important" type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif"
10 10 border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
11 11 <img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
12 12 </form>
13 13 <div style="padding:5px">
14 14 <a href="http://flattr.com/thing/167489/RhodeCode" target="_blank">
15 15 <img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
16 16 </div>
17 17 </div>
18 18 {% endblock %}}
@@ -1,54 +1,79 b''
1 1 .. _general:
2 2
3 3 General RhodeCode usage
4 4 =======================
5 5
6 6
7 7 Repository deleting
8 8 -------------------
9 9
10 10 Currently when admin/owner deletes a repository, RhodeCode does not physically
11 11 delete a repository from filesystem, it renames it in a special way so it's
12 12 not possible to push,clone or access repository. It's worth a notice that,
13 13 even if someone will be given administrative access to RhodeCode and will
14 14 delete a repository You can easy restore such action by restoring `rm__<date>`
15 15 from the repository name, and internal repository storage (.hg/.git)
16 16
17 17 Follow current branch in file view
18 18 ----------------------------------
19 19
20 20 In file view when this checkbox is checked the << and >> arrows will jump
21 21 to changesets within the same branch currently viewing. So for example
22 22 if someone is viewing files at 'beta' branch and marks `follow current branch`
23 23 checkbox the << and >> buttons will only show him revisions for 'beta' branch
24 24
25 25
26 26 Compare view from changelog
27 27 ---------------------------
28 28
29 29 Checkboxes in compare view allow users to view combined compare view. You can
30 30 only show the range between the first and last checkbox (no cherry pick).
31 31 Clicking more than one checkbox will activate a link in top saying
32 32 `Show selected changes <from-rev> -> <to-rev>` clicking this will bring
33 33 compare view
34 34
35 35 Compare view is also available from the journal on pushes having more than
36 36 one changeset
37 37
38 38
39 Non changeable repository urls
40 ------------------------------
41
42 Due to complicated nature of repository grouping, often urls of repositories
43 can change.
44
45 example::
46
47 #before
48 http://server.com/repo_name
49 # after insertion to test_group group the url will be
50 http://server.com/test_group/repo_name
51
52 This can be an issue for build systems and any other hardcoded scripts, moving
53 repository to a group leads to a need for changing external systems. To
54 overcome this RhodeCode introduces a non changable replacement url. It's
55 simply an repository ID prefixed with `_` above urls are also accessible as::
56
57 http://server.com/_<ID>
58
59 Since ID are always the same moving the repository will not affect such url.
60 the _<ID> syntax can be used anywhere in the system so urls with repo_name
61 for changelogs, files and other can be exchanged with _<ID> syntax.
62
63
39 64
40 65 Mailing
41 66 -------
42 67
43 68 When administrator will fill up the mailing settings in .ini files
44 69 RhodeCode will send mails on user registration, or when RhodeCode errors occur
45 70 on errors the mails will have a detailed traceback of error.
46 71
47 72
48 73 Trending source files
49 74 ---------------------
50 75
51 76 Trending source files are calculated based on pre defined dict of known
52 77 types and extensions. If You miss some extension or Would like to scan some
53 78 custom files it's possible to add new types in `LANGUAGES_EXTENSIONS_MAP` dict
54 79 located in `/rhodecode/lib/celerylib/tasks.py` No newline at end of file
@@ -1,24 +1,48 b''
1 .. _enable_git:
1 .. _git_support:
2 2
3 Enabling GIT support (beta)
4 ===========================
3 GIT support
4 ===========
5 5
6 6
7 Git support in RhodeCode 1.1 was disabled due to current instability issues.
8 However,if you would like to test git support please feel free to re-enable it.
9 To re-enable GIT support just uncomment the git line in the
10 file **rhodecode/__init__.py**
7 Git support in RhodeCode 1.3 was enabled by default.
8 Although There are some limitations on git usage.
9
10 - No hooks are runned for git push/pull actions.
11 - logs in action journals don't have git operations
12 - large pushes needs http server with chunked encoding support.
13
14 if you plan to use git you need to run RhodeCode with some
15 http server that supports chunked encoding which git http protocol uses,
16 i recommend using waitress_ or gunicorn_ (linux only) for `paste` wsgi app
17 replacement.
18
19 To use waitress simply change change the following in the .ini file::
20
21 use = egg:Paste#http
22
23 To::
24
25 use = egg:waitress#main
26
27 And comment out bellow options::
28
29 threadpool_workers =
30 threadpool_max_requests =
31 use_threadpool =
32
33
34 You can simply run `paster serve` as usual.
35
36
37 You can always disable git/hg support by editing a
38 file **rhodecode/__init__.py** and commenting out backends
11 39
12 40 .. code-block:: python
13 41
14 42 BACKENDS = {
15 43 'hg': 'Mercurial repository',
16 44 #'git': 'Git repository',
17 45 }
18 46
19 .. note::
20 Please note that the git support provided by RhodeCode is not yet fully
21 stable and RhodeCode might crash while using git repositories. (That is why
22 it is currently disabled.) Thus be careful about enabling git support, and
23 certainly don't use it in a production setting!
24 No newline at end of file
47 .. _waitress: http://pypi.python.org/pypi/waitress
48 .. _gunicorn: http://pypi.python.org/pypi/gunicorn No newline at end of file
1 NO CONTENT: modified file chmod 100755 => 100644
@@ -1,235 +1,295 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
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 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 #email_prefix = [RhodeCode]
20 21
21 22 #smtp_server = mail.server.com
22 23 #smtp_username =
23 24 #smtp_password =
24 25 #smtp_port =
25 26 #smtp_use_tls = false
26 27 #smtp_use_ssl = true
27 28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 29 #smtp_auth =
29 30
30 31 [server:main]
31 32 ##nr of threads to spawn
32 33 threadpool_workers = 5
33 34
34 35 ##max request before thread respawn
35 36 threadpool_max_requests = 10
36 37
37 38 ##option to use threads of process
38 39 use_threadpool = true
39 40
40 41 use = egg:Paste#http
41 42 host = 127.0.0.1
42 43 port = 8001
43 44
44 45 [app:main]
45 46 use = egg:rhodecode
46 47 full_stack = true
47 48 static_files = true
48 lang=en
49 lang = en
49 50 cache_dir = %(here)s/data
50 51 index_dir = %(here)s/data/index
51 app_instance_uuid = prod1234
52 app_instance_uuid = rc-production
52 53 cut_off_limit = 256000
53 force_https = false
54 force_https = false
54 55 commit_parse_limit = 50
55 56 use_gravatar = true
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
59 default_encoding = utf8
60
61 ## overwrite schema of clone url
62 ## available vars:
63 ## scheme - http/https
64 ## user - current user
65 ## pass - password
66 ## netloc - network location
67 ## path - usually repo_name
68
69 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70
71 ## issue tracking mapping for commits messages
72 ## comment out issue_pat, issue_server, issue_prefix to enable
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #<numbers> with a regex passive group for `#`
76 ## {id} will be all groups matched from this pattern
77
78 issue_pat = (?:\s*#)(\d+)
79
80 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
82
83 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84
85 ## prefix to add to link to indicate it's an url
86 ## #314 will be replaced by <issue_prefix><id>
87
88 issue_prefix = #
89
90 ## instance-id prefix
91 ## a prefix key for this instance used for cache invalidation when running
92 ## multiple instances of rhodecode, make sure it's globally unique for
93 ## all running rhodecode instances. Leave empty if you don't use it
94 instance_id =
56 95
57 96 ####################################
58 97 ### CELERY CONFIG ####
59 98 ####################################
60 99 use_celery = false
61 100 broker.host = localhost
62 101 broker.vhost = rabbitmqhost
63 102 broker.port = 5672
64 103 broker.user = rabbitmq
65 104 broker.password = qweqwe
66 105
67 106 celery.imports = rhodecode.lib.celerylib.tasks
68 107
69 108 celery.result.backend = amqp
70 109 celery.result.dburi = amqp://
71 110 celery.result.serialier = json
72 111
73 112 #celery.send.task.error.emails = true
74 113 #celery.amqp.task.result.expires = 18000
75 114
76 115 celeryd.concurrency = 2
77 116 #celeryd.log.file = celeryd.log
78 117 celeryd.log.level = debug
79 118 celeryd.max.tasks.per.child = 1
80 119
81 120 #tasks will never be sent to the queue, but executed locally instead.
82 121 celery.always.eager = false
83 122
84 123 ####################################
85 124 ### BEAKER CACHE ####
86 125 ####################################
87 126 beaker.cache.data_dir=%(here)s/data/cache/data
88 127 beaker.cache.lock_dir=%(here)s/data/cache/lock
89 128
90 129 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
91 130
92 131 beaker.cache.super_short_term.type=memory
93 132 beaker.cache.super_short_term.expire=10
133 beaker.cache.super_short_term.key_length = 256
94 134
95 135 beaker.cache.short_term.type=memory
96 136 beaker.cache.short_term.expire=60
137 beaker.cache.short_term.key_length = 256
97 138
98 139 beaker.cache.long_term.type=memory
99 140 beaker.cache.long_term.expire=36000
141 beaker.cache.long_term.key_length = 256
100 142
101 143 beaker.cache.sql_cache_short.type=memory
102 144 beaker.cache.sql_cache_short.expire=10
145 beaker.cache.sql_cache_short.key_length = 256
103 146
104 147 beaker.cache.sql_cache_med.type=memory
105 148 beaker.cache.sql_cache_med.expire=360
149 beaker.cache.sql_cache_med.key_length = 256
106 150
107 151 beaker.cache.sql_cache_long.type=file
108 152 beaker.cache.sql_cache_long.expire=3600
153 beaker.cache.sql_cache_long.key_length = 256
109 154
110 155 ####################################
111 156 ### BEAKER SESSION ####
112 157 ####################################
113 158 ## Type of storage used for the session, current types are
114 159 ## dbm, file, memcached, database, and memory.
115 160 ## The storage uses the Container API
116 ##that is also used by the cache system.
117 beaker.session.type = file
161 ## that is also used by the cache system.
162
163 ## db session example
164
165 #beaker.session.type = ext:database
166 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
167 #beaker.session.table_name = db_session
168
169 ## encrypted cookie session, good for many instances
170 #beaker.session.type = cookie
118 171
172 beaker.session.type = file
119 173 beaker.session.key = rhodecode
120 beaker.session.secret = g654dcno0-9873jhgfreyu
174 # secure cookie requires AES python libraries
175 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
176 #beaker.session.validate_key = 9712sds2212c--zxc123
121 177 beaker.session.timeout = 36000
178 beaker.session.httponly = true
179
180 ## uncomment for https secure cookie
181 beaker.session.secure = false
122 182
123 183 ##auto save the session to not to use .save()
124 184 beaker.session.auto = False
125 185
126 186 ##true exire at browser close
127 187 #beaker.session.cookie_expires = 3600
128 188
129
189
130 190 ################################################################################
131 191 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
132 192 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
133 193 ## execute malicious code after an exception is raised. ##
134 194 ################################################################################
135 195 set debug = false
136 196
137 197 ##################################
138 198 ### LOGVIEW CONFIG ###
139 199 ##################################
140 200 logview.sqlalchemy = #faa
141 201 logview.pylons.templating = #bfb
142 202 logview.pylons.util = #eee
143 203
144 204 #########################################################
145 205 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
146 206 #########################################################
147 207 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
148 208 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
149 209 sqlalchemy.db1.echo = false
150 210 sqlalchemy.db1.pool_recycle = 3600
151 211 sqlalchemy.convert_unicode = true
152 212
153 213 ################################
154 214 ### LOGGING CONFIGURATION ####
155 215 ################################
156 216 [loggers]
157 217 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
158 218
159 219 [handlers]
160 220 keys = console, console_sql
161 221
162 222 [formatters]
163 223 keys = generic, color_formatter, color_formatter_sql
164 224
165 225 #############
166 226 ## LOGGERS ##
167 227 #############
168 228 [logger_root]
169 229 level = NOTSET
170 230 handlers = console
171 231
172 232 [logger_routes]
173 233 level = DEBUG
174 234 handlers =
175 235 qualname = routes.middleware
176 236 # "level = DEBUG" logs the route matched and routing variables.
177 237 propagate = 1
178 238
179 239 [logger_beaker]
180 240 level = DEBUG
181 241 handlers =
182 242 qualname = beaker.container
183 243 propagate = 1
184 244
185 245 [logger_templates]
186 246 level = INFO
187 247 handlers =
188 248 qualname = pylons.templating
189 249 propagate = 1
190 250
191 251 [logger_rhodecode]
192 252 level = DEBUG
193 253 handlers =
194 254 qualname = rhodecode
195 255 propagate = 1
196 256
197 257 [logger_sqlalchemy]
198 258 level = INFO
199 259 handlers = console_sql
200 260 qualname = sqlalchemy.engine
201 261 propagate = 0
202 262
203 263 ##############
204 264 ## HANDLERS ##
205 265 ##############
206 266
207 267 [handler_console]
208 268 class = StreamHandler
209 269 args = (sys.stderr,)
210 270 level = INFO
211 271 formatter = generic
212 272
213 273 [handler_console_sql]
214 274 class = StreamHandler
215 275 args = (sys.stderr,)
216 276 level = WARN
217 277 formatter = generic
218 278
219 279 ################
220 280 ## FORMATTERS ##
221 281 ################
222 282
223 283 [formatter_generic]
224 284 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
225 285 datefmt = %Y-%m-%d %H:%M:%S
226 286
227 287 [formatter_color_formatter]
228 288 class=rhodecode.lib.colored_formatter.ColorFormatter
229 289 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
230 290 datefmt = %Y-%m-%d %H:%M:%S
231 291
232 292 [formatter_color_formatter_sql]
233 293 class=rhodecode.lib.colored_formatter.ColorFormatterSql
234 294 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
235 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
295 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,16 +1,17 b''
1 1 Pylons==1.0.0
2 Beaker==1.5.4
2 Beaker==1.6.2
3 3 WebHelpers>=1.2
4 4 formencode==1.2.4
5 5 SQLAlchemy==0.7.4
6 6 Mako==0.5.0
7 7 pygments>=1.4
8 whoosh<1.8
8 whoosh>=2.3.0,<2.4
9 9 celery>=2.2.5,<2.3
10 10 babel
11 11 python-dateutil>=1.5.0,<2.0.0
12 12 dulwich>=0.8.0,<0.9.0
13 vcs==0.2.2
14 13 webob==1.0.8
14 markdown==2.1.1
15 docutils==0.8.1
15 16 py-bcrypt
16 mercurial==2.0.2 No newline at end of file
17 mercurial>=2.1,<2.2 No newline at end of file
@@ -1,86 +1,92 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.__init__
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode, a web based repository management based on pylons
7 7 versioning implementation: http://semver.org/
8 8
9 9 :created_on: Apr 9, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import sys
27 27 import platform
28 28
29 VERSION = (1, 2, 5)
29 VERSION = (1, 3, 0)
30 30 __version__ = '.'.join((str(each) for each in VERSION[:4]))
31 __dbversion__ = 3 # defines current db version for migrations
31 __dbversion__ = 5 # defines current db version for migrations
32 32 __platform__ = platform.system()
33 33 __license__ = 'GPLv3'
34 34 __py_version__ = sys.version_info
35 35
36 36 PLATFORM_WIN = ('Windows')
37 37 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
38 38
39 39 requirements = [
40 40 "Pylons==1.0.0",
41 "Beaker==1.5.4",
41 "Beaker==1.6.2",
42 42 "WebHelpers>=1.2",
43 43 "formencode==1.2.4",
44 44 "SQLAlchemy==0.7.4",
45 45 "Mako==0.5.0",
46 46 "pygments>=1.4",
47 "whoosh<1.8",
47 "whoosh>=2.3.0,<2.4",
48 48 "celery>=2.2.5,<2.3",
49 49 "babel",
50 50 "python-dateutil>=1.5.0,<2.0.0",
51 51 "dulwich>=0.8.0,<0.9.0",
52 "vcs==0.2.2",
53 "webob==1.0.8"
52 "webob==1.0.8",
53 "markdown==2.1.1",
54 "docutils==0.8.1",
54 55 ]
55 56
56 57 if __py_version__ < (2, 6):
57 58 requirements.append("simplejson")
58 59 requirements.append("pysqlite")
59 60
60 61 if __platform__ in PLATFORM_WIN:
61 requirements.append("mercurial==2.0.1")
62 requirements.append("mercurial>=2.1,<2.2")
62 63 else:
63 64 requirements.append("py-bcrypt")
64 requirements.append("mercurial==2.0.2")
65 requirements.append("mercurial>=2.1,<2.2")
65 66
66 67
67 68 try:
68 69 from rhodecode.lib import get_current_revision
69 _rev = get_current_revision(quiet=True)
70 _rev = get_current_revision()
70 71 except ImportError:
71 72 # this is needed when doing some setup.py operations
72 73 _rev = False
73 74
74 75 if len(VERSION) > 3 and _rev:
75 76 __version__ += ' [rev:%s]' % _rev[0]
76 77
77 78
78 79 def get_version():
79 80 """Returns shorter version (digit parts only) as string."""
80 81
81 82 return '.'.join((str(each) for each in VERSION[:3]))
82 83
83 84 BACKENDS = {
84 85 'hg': 'Mercurial repository',
85 #'git': 'Git repository',
86 'git': 'Git repository',
86 87 }
88
89 CELERY_ON = False
90
91 # link to config for pylons
92 CONFIG = {}
@@ -1,244 +1,305 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
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 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 #email_prefix = [RhodeCode]
20 21
21 22 #smtp_server = mail.server.com
22 23 #smtp_username =
23 24 #smtp_password =
24 25 #smtp_port =
25 26 #smtp_use_tls = false
26 27 #smtp_use_ssl = true
27 28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 29 #smtp_auth =
29 30
30 31 [server:main]
31 32 ##nr of threads to spawn
32 33 threadpool_workers = 5
33 34
34 35 ##max request before thread respawn
35 36 threadpool_max_requests = 10
36 37
37 38 ##option to use threads of process
38 39 use_threadpool = true
39 40
40 41 use = egg:Paste#http
41 42 host = 127.0.0.1
42 43 port = 5000
43 44
44 45 [app:main]
45 46 use = egg:rhodecode
46 47 full_stack = true
47 48 static_files = true
48 lang=en
49 lang = en
49 50 cache_dir = %(here)s/data
50 51 index_dir = %(here)s/data/index
51 52 app_instance_uuid = ${app_instance_uuid}
52 53 cut_off_limit = 256000
53 force_https = false
54 force_https = false
54 55 commit_parse_limit = 50
55 56 use_gravatar = true
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
59 default_encoding = utf8
60
61 ## overwrite schema of clone url
62 ## available vars:
63 ## scheme - http/https
64 ## user - current user
65 ## pass - password
66 ## netloc - network location
67 ## path - usually repo_name
68
69 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70
71 ## issue tracking mapping for commits messages
72 ## comment out issue_pat, issue_server, issue_prefix to enable
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #<numbers> with a regex passive group for `#`
76 ## {id} will be all groups matched from this pattern
77
78 issue_pat = (?:\s*#)(\d+)
79
80 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
82
83 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84
85 ## prefix to add to link to indicate it's an url
86 ## #314 will be replaced by <issue_prefix><id>
87
88 issue_prefix = #
89
90 ## instance-id prefix
91 ## a prefix key for this instance used for cache invalidation when running
92 ## multiple instances of rhodecode, make sure it's globally unique for
93 ## all running rhodecode instances. Leave empty if you don't use it
94 instance_id =
56 95
57 96 ####################################
58 97 ### CELERY CONFIG ####
59 98 ####################################
60 99 use_celery = false
61 100 broker.host = localhost
62 101 broker.vhost = rabbitmqhost
63 102 broker.port = 5672
64 103 broker.user = rabbitmq
65 104 broker.password = qweqwe
66 105
67 106 celery.imports = rhodecode.lib.celerylib.tasks
68 107
69 108 celery.result.backend = amqp
70 109 celery.result.dburi = amqp://
71 110 celery.result.serialier = json
72 111
73 112 #celery.send.task.error.emails = true
74 113 #celery.amqp.task.result.expires = 18000
75 114
76 115 celeryd.concurrency = 2
77 116 #celeryd.log.file = celeryd.log
78 117 celeryd.log.level = debug
79 118 celeryd.max.tasks.per.child = 1
80 119
81 120 #tasks will never be sent to the queue, but executed locally instead.
82 121 celery.always.eager = false
83 122
84 123 ####################################
85 124 ### BEAKER CACHE ####
86 125 ####################################
87 126 beaker.cache.data_dir=%(here)s/data/cache/data
88 127 beaker.cache.lock_dir=%(here)s/data/cache/lock
89 128
90 129 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
91 130
92 131 beaker.cache.super_short_term.type=memory
93 132 beaker.cache.super_short_term.expire=10
133 beaker.cache.super_short_term.key_length = 256
94 134
95 135 beaker.cache.short_term.type=memory
96 136 beaker.cache.short_term.expire=60
137 beaker.cache.short_term.key_length = 256
97 138
98 139 beaker.cache.long_term.type=memory
99 140 beaker.cache.long_term.expire=36000
141 beaker.cache.long_term.key_length = 256
100 142
101 143 beaker.cache.sql_cache_short.type=memory
102 144 beaker.cache.sql_cache_short.expire=10
145 beaker.cache.sql_cache_short.key_length = 256
103 146
104 147 beaker.cache.sql_cache_med.type=memory
105 148 beaker.cache.sql_cache_med.expire=360
149 beaker.cache.sql_cache_med.key_length = 256
106 150
107 151 beaker.cache.sql_cache_long.type=file
108 152 beaker.cache.sql_cache_long.expire=3600
153 beaker.cache.sql_cache_long.key_length = 256
109 154
110 155 ####################################
111 156 ### BEAKER SESSION ####
112 157 ####################################
113 158 ## Type of storage used for the session, current types are
114 159 ## dbm, file, memcached, database, and memory.
115 160 ## The storage uses the Container API
116 ##that is also used by the cache system.
117 beaker.session.type = file
161 ## that is also used by the cache system.
162
163 ## db session example
164
165 #beaker.session.type = ext:database
166 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
167 #beaker.session.table_name = db_session
168
169 ## encrypted cookie session, good for many instances
170 #beaker.session.type = cookie
118 171
172 beaker.session.type = file
119 173 beaker.session.key = rhodecode
120 beaker.session.secret = ${app_instance_secret}
174 # secure cookie requires AES python libraries
175 #beaker.session.encrypt_key = ${app_instance_secret}
176 #beaker.session.validate_key = ${app_instance_secret}
121 177 beaker.session.timeout = 36000
178 beaker.session.httponly = true
179
180 ## uncomment for https secure cookie
181 beaker.session.secure = false
122 182
123 183 ##auto save the session to not to use .save()
124 184 beaker.session.auto = False
125 185
126 186 ##true exire at browser close
127 187 #beaker.session.cookie_expires = 3600
128 188
129
189
130 190 ################################################################################
131 191 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
132 192 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
133 193 ## execute malicious code after an exception is raised. ##
134 194 ################################################################################
135 195 set debug = false
136 196
137 197 ##################################
138 198 ### LOGVIEW CONFIG ###
139 199 ##################################
140 200 logview.sqlalchemy = #faa
141 201 logview.pylons.templating = #bfb
142 202 logview.pylons.util = #eee
143 203
144 204 #########################################################
145 205 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
146 206 #########################################################
147 207
148 208 # SQLITE [default]
149 209 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
150 210
151 211 # POSTGRESQL
152 212 # sqlalchemy.db1.url = postgresql://user:pass@localhost/rhodecode
153 213
154 214 # MySQL
155 215 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
156 216
217 # see sqlalchemy docs for others
157 218
158 219 sqlalchemy.db1.echo = false
159 220 sqlalchemy.db1.pool_recycle = 3600
160 221 sqlalchemy.convert_unicode = true
161 222
162 223 ################################
163 224 ### LOGGING CONFIGURATION ####
164 225 ################################
165 226 [loggers]
166 227 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
167 228
168 229 [handlers]
169 230 keys = console, console_sql
170 231
171 232 [formatters]
172 233 keys = generic, color_formatter, color_formatter_sql
173 234
174 235 #############
175 236 ## LOGGERS ##
176 237 #############
177 238 [logger_root]
178 239 level = NOTSET
179 240 handlers = console
180 241
181 242 [logger_routes]
182 243 level = DEBUG
183 244 handlers =
184 245 qualname = routes.middleware
185 246 # "level = DEBUG" logs the route matched and routing variables.
186 247 propagate = 1
187 248
188 249 [logger_beaker]
189 250 level = DEBUG
190 251 handlers =
191 252 qualname = beaker.container
192 253 propagate = 1
193 254
194 255 [logger_templates]
195 256 level = INFO
196 257 handlers =
197 258 qualname = pylons.templating
198 259 propagate = 1
199 260
200 261 [logger_rhodecode]
201 262 level = DEBUG
202 263 handlers =
203 264 qualname = rhodecode
204 265 propagate = 1
205 266
206 267 [logger_sqlalchemy]
207 268 level = INFO
208 269 handlers = console_sql
209 270 qualname = sqlalchemy.engine
210 271 propagate = 0
211 272
212 273 ##############
213 274 ## HANDLERS ##
214 275 ##############
215 276
216 277 [handler_console]
217 278 class = StreamHandler
218 279 args = (sys.stderr,)
219 280 level = INFO
220 formatter = color_formatter
281 formatter = generic
221 282
222 283 [handler_console_sql]
223 284 class = StreamHandler
224 285 args = (sys.stderr,)
225 286 level = WARN
226 formatter = color_formatter_sql
287 formatter = generic
227 288
228 289 ################
229 290 ## FORMATTERS ##
230 291 ################
231 292
232 293 [formatter_generic]
233 294 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
234 295 datefmt = %Y-%m-%d %H:%M:%S
235 296
236 297 [formatter_color_formatter]
237 298 class=rhodecode.lib.colored_formatter.ColorFormatter
238 299 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
239 300 datefmt = %Y-%m-%d %H:%M:%S
240 301
241 302 [formatter_color_formatter_sql]
242 303 class=rhodecode.lib.colored_formatter.ColorFormatterSql
243 304 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
244 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
305 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,80 +1,87 b''
1 1 """Pylons environment configuration"""
2 2
3 3 import os
4 4 import logging
5 5
6 6 from mako.lookup import TemplateLookup
7 7 from pylons.configuration import PylonsConfig
8 8 from pylons.error import handle_mako_error
9 9
10 import rhodecode
10 11 import rhodecode.lib.app_globals as app_globals
11 12 import rhodecode.lib.helpers
12 13
13 14 from rhodecode.config.routing import make_map
14 from rhodecode.lib import celerypylons
15 # don't remove this import it does magic for celery
16 from rhodecode.lib import celerypylons, str2bool
15 17 from rhodecode.lib import engine_from_config
16 from rhodecode.lib.timerproxy import TimerProxy
17 18 from rhodecode.lib.auth import set_available_permissions
18 19 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config
19 20 from rhodecode.model import init_model
20 21 from rhodecode.model.scm import ScmModel
21 22
22 23 log = logging.getLogger(__name__)
23 24
24 25
25 26 def load_environment(global_conf, app_conf, initial=False):
26 27 """Configure the Pylons environment via the ``pylons.config``
27 28 object
28 29 """
29 30 config = PylonsConfig()
30 31
31 32 # Pylons paths
32 33 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
33 34 paths = dict(root=root,
34 35 controllers=os.path.join(root, 'controllers'),
35 36 static_files=os.path.join(root, 'public'),
36 37 templates=[os.path.join(root, 'templates')])
37 38
38 39 # Initialize config with the basic options
39 40 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
40 41
42 # store some globals into rhodecode
43 rhodecode.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
44
41 45 config['routes.map'] = make_map(config)
42 46 config['pylons.app_globals'] = app_globals.Globals(config)
43 47 config['pylons.h'] = rhodecode.lib.helpers
44
48 rhodecode.CONFIG = config
45 49 # Setup cache object as early as possible
46 50 import pylons
47 51 pylons.cache._push_object(config['pylons.app_globals'].cache)
48 52
49 53 # Create the Mako TemplateLookup, with the default auto-escaping
50 54 config['pylons.app_globals'].mako_lookup = TemplateLookup(
51 55 directories=paths['templates'],
52 56 error_handler=handle_mako_error,
53 57 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
54 58 input_encoding='utf-8', default_filters=['escape'],
55 59 imports=['from webhelpers.html import escape'])
56 60
57 #sets the c attribute access when don't existing attribute are accessed
61 # sets the c attribute access when don't existing attribute are accessed
58 62 config['pylons.strict_tmpl_context'] = True
59 63 test = os.path.split(config['__file__'])[-1] == 'test.ini'
60 64 if test:
61 65 from rhodecode.lib.utils import create_test_env, create_test_index
62 66 from rhodecode.tests import TESTS_TMP_PATH
63 67 create_test_env(TESTS_TMP_PATH, config)
64 68 create_test_index(TESTS_TMP_PATH, config, True)
65 69
66 #MULTIPLE DB configs
70 # MULTIPLE DB configs
67 71 # Setup the SQLAlchemy database engine
68 72 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
69 73
70 74 init_model(sa_engine_db1)
71 75
72 76 repos_path = make_ui('db').configitems('paths')[0][1]
73 77 repo2db_mapper(ScmModel().repo_scan(repos_path))
74 78 set_available_permissions(config)
75 79 config['base_path'] = repos_path
76 80 set_rhodecode_config(config)
77 81 # CONFIGURATION OPTIONS HERE (note: all config options will override
78 82 # any Pylons config options)
79 83
84 # store config reference into our module to skip import magic of
85 # pylons
86 rhodecode.CONFIG.update(config)
80 87 return config
@@ -1,84 +1,85 b''
1 1 """Pylons middleware initialization"""
2 2
3 3 from beaker.middleware import SessionMiddleware
4 4 from routes.middleware import RoutesMiddleware
5 5 from paste.cascade import Cascade
6 6 from paste.registry import RegistryManager
7 7 from paste.urlparser import StaticURLParser
8 8 from paste.deploy.converters import asbool
9 9 from paste.gzipper import make_gzip_middleware
10 10
11 11 from pylons.middleware import ErrorHandler, StatusCodeRedirect
12 12 from pylons.wsgiapp import PylonsApp
13 13
14 14 from rhodecode.lib.middleware.simplehg import SimpleHg
15 15 from rhodecode.lib.middleware.simplegit import SimpleGit
16 16 from rhodecode.lib.middleware.https_fixup import HttpsFixup
17 17 from rhodecode.config.environment import load_environment
18 18
19 19
20 20 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
21 21 """Create a Pylons WSGI application and return it
22 22
23 23 ``global_conf``
24 24 The inherited configuration for this application. Normally from
25 25 the [DEFAULT] section of the Paste ini file.
26 26
27 27 ``full_stack``
28 28 Whether or not this application provides a full WSGI stack (by
29 29 default, meaning it handles its own exceptions and errors).
30 30 Disable full_stack when this application is "managed" by
31 31 another WSGI middleware.
32 32
33 33 ``app_conf``
34 34 The application's local configuration. Normally specified in
35 35 the [app:<name>] section of the Paste ini file (where <name>
36 36 defaults to main).
37 37
38 38 """
39 39 # Configure the Pylons environment
40 40 config = load_environment(global_conf, app_conf)
41 41
42 42 # The Pylons WSGI app
43 43 app = PylonsApp(config=config)
44 44
45 45 # Routing/Session/Cache Middleware
46 46 app = RoutesMiddleware(app, config['routes.map'])
47 47 app = SessionMiddleware(app, config)
48 48
49 49 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
50 50 if asbool(config['pdebug']):
51 51 from rhodecode.lib.profiler import ProfilingMiddleware
52 52 app = ProfilingMiddleware(app)
53 53
54 # we want our low level middleware to get to the request ASAP. We don't
55 # need any pylons stack middleware in them
56 app = SimpleHg(app, config)
57 app = SimpleGit(app, config)
54 if asbool(full_stack):
58 55
59 if asbool(full_stack):
60 56 # Handle Python exceptions
61 57 app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
62 58
59 # we want our low level middleware to get to the request ASAP. We don't
60 # need any pylons stack middleware in them
61 app = SimpleHg(app, config)
62 app = SimpleGit(app, config)
63
63 64 # Display error documents for 401, 403, 404 status codes (and
64 65 # 500 when debug is disabled)
65 66 if asbool(config['debug']):
66 67 app = StatusCodeRedirect(app)
67 68 else:
68 69 app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
69 70
70 71 #enable https redirets based on HTTP_X_URL_SCHEME set by proxy
71 72 app = HttpsFixup(app, config)
72 73
73 74 # Establish the Registry for this application
74 75 app = RegistryManager(app)
75 76
76 77 if asbool(static_files):
77 78 # Serve static files
78 79 static_app = StaticURLParser(config['pylons.paths']['static_files'])
79 80 app = Cascade([static_app, app])
80 81 app = make_gzip_middleware(app, global_conf, compress_level=1)
81 82
82 83 app.config = config
83 84
84 85 return app
@@ -1,441 +1,507 b''
1 1 """
2 2 Routes configuration
3 3
4 4 The more specific and detailed routes should be defined first so they
5 5 may take precedent over the more generic routes. For more information
6 6 refer to the routes manual at http://routes.groovie.org/docs/
7 7 """
8 8 from __future__ import with_statement
9 9 from routes import Mapper
10 10
11
12 11 # prefix for non repository related links needs to be prefixed with `/`
13 12 ADMIN_PREFIX = '/_admin'
14 13
15 14
16 15 def make_map(config):
17 16 """Create, configure and return the routes Mapper"""
18 17 rmap = Mapper(directory=config['pylons.paths']['controllers'],
19 18 always_scan=config['debug'])
20 19 rmap.minimization = False
21 20 rmap.explicit = False
22 21
23 22 from rhodecode.lib.utils import is_valid_repo
24 23 from rhodecode.lib.utils import is_valid_repos_group
25 24
26 25 def check_repo(environ, match_dict):
27 26 """
28 27 check for valid repository for proper 404 handling
29
28
30 29 :param environ:
31 30 :param match_dict:
32 31 """
32 from rhodecode.model.db import Repository
33 repo_name = match_dict.get('repo_name')
33 34
34 repo_name = match_dict.get('repo_name')
35 try:
36 by_id = repo_name.split('_')
37 if len(by_id) == 2 and by_id[1].isdigit():
38 repo_name = Repository.get(by_id[1]).repo_name
39 match_dict['repo_name'] = repo_name
40 except:
41 pass
42
35 43 return is_valid_repo(repo_name, config['base_path'])
36 44
37 45 def check_group(environ, match_dict):
38 46 """
39 47 check for valid repositories group for proper 404 handling
40
48
41 49 :param environ:
42 50 :param match_dict:
43 51 """
44 52 repos_group_name = match_dict.get('group_name')
45 53
46 54 return is_valid_repos_group(repos_group_name, config['base_path'])
47 55
48
49 56 def check_int(environ, match_dict):
50 57 return match_dict.get('id').isdigit()
51 58
52 59 # The ErrorController route (handles 404/500 error pages); it should
53 60 # likely stay at the top, ensuring it can always be resolved
54 61 rmap.connect('/error/{action}', controller='error')
55 62 rmap.connect('/error/{action}/{id}', controller='error')
56 63
57 64 #==========================================================================
58 65 # CUSTOM ROUTES HERE
59 66 #==========================================================================
60 67
61 68 #MAIN PAGE
62 69 rmap.connect('home', '/', controller='home', action='index')
63 70 rmap.connect('repo_switcher', '/repos', controller='home',
64 71 action='repo_switcher')
72 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
73 controller='home', action='branch_tag_switcher')
65 74 rmap.connect('bugtracker',
66 75 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
67 76 _static=True)
77 rmap.connect('rst_help',
78 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
79 _static=True)
68 80 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
69 81
70 82 #ADMIN REPOSITORY REST ROUTES
71 83 with rmap.submapper(path_prefix=ADMIN_PREFIX,
72 84 controller='admin/repos') as m:
73 85 m.connect("repos", "/repos",
74 86 action="create", conditions=dict(method=["POST"]))
75 87 m.connect("repos", "/repos",
76 88 action="index", conditions=dict(method=["GET"]))
77 89 m.connect("formatted_repos", "/repos.{format}",
78 90 action="index",
79 91 conditions=dict(method=["GET"]))
80 92 m.connect("new_repo", "/repos/new",
81 93 action="new", conditions=dict(method=["GET"]))
82 94 m.connect("formatted_new_repo", "/repos/new.{format}",
83 95 action="new", conditions=dict(method=["GET"]))
84 96 m.connect("/repos/{repo_name:.*}",
85 97 action="update", conditions=dict(method=["PUT"],
86 98 function=check_repo))
87 99 m.connect("/repos/{repo_name:.*}",
88 100 action="delete", conditions=dict(method=["DELETE"],
89 101 function=check_repo))
90 102 m.connect("edit_repo", "/repos/{repo_name:.*}/edit",
91 103 action="edit", conditions=dict(method=["GET"],
92 104 function=check_repo))
93 105 m.connect("formatted_edit_repo", "/repos/{repo_name:.*}.{format}/edit",
94 106 action="edit", conditions=dict(method=["GET"],
95 107 function=check_repo))
96 108 m.connect("repo", "/repos/{repo_name:.*}",
97 109 action="show", conditions=dict(method=["GET"],
98 110 function=check_repo))
99 111 m.connect("formatted_repo", "/repos/{repo_name:.*}.{format}",
100 112 action="show", conditions=dict(method=["GET"],
101 113 function=check_repo))
102 114 #ajax delete repo perm user
103 115 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}",
104 action="delete_perm_user", conditions=dict(method=["DELETE"],
105 function=check_repo))
116 action="delete_perm_user",
117 conditions=dict(method=["DELETE"], function=check_repo))
118
106 119 #ajax delete repo perm users_group
107 120 m.connect('delete_repo_users_group',
108 121 "/repos_delete_users_group/{repo_name:.*}",
109 122 action="delete_perm_users_group",
110 123 conditions=dict(method=["DELETE"], function=check_repo))
111 124
112 125 #settings actions
113 126 m.connect('repo_stats', "/repos_stats/{repo_name:.*}",
114 action="repo_stats", conditions=dict(method=["DELETE"],
115 function=check_repo))
127 action="repo_stats", conditions=dict(method=["DELETE"],
128 function=check_repo))
116 129 m.connect('repo_cache', "/repos_cache/{repo_name:.*}",
117 action="repo_cache", conditions=dict(method=["DELETE"],
130 action="repo_cache", conditions=dict(method=["DELETE"],
131 function=check_repo))
132 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*}",
133 action="repo_public_journal", conditions=dict(method=["PUT"],
118 134 function=check_repo))
119 m.connect('repo_public_journal',
120 "/repos_public_journal/{repo_name:.*}",
121 action="repo_public_journal", conditions=dict(method=["PUT"],
122 function=check_repo))
123 135 m.connect('repo_pull', "/repo_pull/{repo_name:.*}",
124 action="repo_pull", conditions=dict(method=["PUT"],
125 function=check_repo))
136 action="repo_pull", conditions=dict(method=["PUT"],
137 function=check_repo))
138 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*}",
139 action="repo_as_fork", conditions=dict(method=["PUT"],
140 function=check_repo))
126 141
127 142 with rmap.submapper(path_prefix=ADMIN_PREFIX,
128 143 controller='admin/repos_groups') as m:
129 144 m.connect("repos_groups", "/repos_groups",
130 145 action="create", conditions=dict(method=["POST"]))
131 146 m.connect("repos_groups", "/repos_groups",
132 147 action="index", conditions=dict(method=["GET"]))
133 148 m.connect("formatted_repos_groups", "/repos_groups.{format}",
134 149 action="index", conditions=dict(method=["GET"]))
135 150 m.connect("new_repos_group", "/repos_groups/new",
136 151 action="new", conditions=dict(method=["GET"]))
137 152 m.connect("formatted_new_repos_group", "/repos_groups/new.{format}",
138 153 action="new", conditions=dict(method=["GET"]))
139 154 m.connect("update_repos_group", "/repos_groups/{id}",
140 155 action="update", conditions=dict(method=["PUT"],
141 156 function=check_int))
142 157 m.connect("delete_repos_group", "/repos_groups/{id}",
143 158 action="delete", conditions=dict(method=["DELETE"],
144 159 function=check_int))
145 160 m.connect("edit_repos_group", "/repos_groups/{id}/edit",
146 161 action="edit", conditions=dict(method=["GET"],
147 162 function=check_int))
148 163 m.connect("formatted_edit_repos_group",
149 164 "/repos_groups/{id}.{format}/edit",
150 165 action="edit", conditions=dict(method=["GET"],
151 166 function=check_int))
152 167 m.connect("repos_group", "/repos_groups/{id}",
153 168 action="show", conditions=dict(method=["GET"],
154 169 function=check_int))
155 170 m.connect("formatted_repos_group", "/repos_groups/{id}.{format}",
156 171 action="show", conditions=dict(method=["GET"],
157 172 function=check_int))
173 # ajax delete repos group perm user
174 m.connect('delete_repos_group_user_perm',
175 "/delete_repos_group_user_perm/{group_name:.*}",
176 action="delete_repos_group_user_perm",
177 conditions=dict(method=["DELETE"], function=check_group))
178
179 # ajax delete repos group perm users_group
180 m.connect('delete_repos_group_users_group_perm',
181 "/delete_repos_group_users_group_perm/{group_name:.*}",
182 action="delete_repos_group_users_group_perm",
183 conditions=dict(method=["DELETE"], function=check_group))
158 184
159 185 #ADMIN USER REST ROUTES
160 186 with rmap.submapper(path_prefix=ADMIN_PREFIX,
161 187 controller='admin/users') as m:
162 188 m.connect("users", "/users",
163 189 action="create", conditions=dict(method=["POST"]))
164 190 m.connect("users", "/users",
165 191 action="index", conditions=dict(method=["GET"]))
166 192 m.connect("formatted_users", "/users.{format}",
167 193 action="index", conditions=dict(method=["GET"]))
168 194 m.connect("new_user", "/users/new",
169 195 action="new", conditions=dict(method=["GET"]))
170 196 m.connect("formatted_new_user", "/users/new.{format}",
171 197 action="new", conditions=dict(method=["GET"]))
172 198 m.connect("update_user", "/users/{id}",
173 199 action="update", conditions=dict(method=["PUT"]))
174 200 m.connect("delete_user", "/users/{id}",
175 201 action="delete", conditions=dict(method=["DELETE"]))
176 202 m.connect("edit_user", "/users/{id}/edit",
177 203 action="edit", conditions=dict(method=["GET"]))
178 204 m.connect("formatted_edit_user",
179 205 "/users/{id}.{format}/edit",
180 206 action="edit", conditions=dict(method=["GET"]))
181 207 m.connect("user", "/users/{id}",
182 208 action="show", conditions=dict(method=["GET"]))
183 209 m.connect("formatted_user", "/users/{id}.{format}",
184 210 action="show", conditions=dict(method=["GET"]))
185 211
186 212 #EXTRAS USER ROUTES
187 213 m.connect("user_perm", "/users_perm/{id}",
188 214 action="update_perm", conditions=dict(method=["PUT"]))
189 215
190 216 #ADMIN USERS REST ROUTES
191 217 with rmap.submapper(path_prefix=ADMIN_PREFIX,
192 218 controller='admin/users_groups') as m:
193 219 m.connect("users_groups", "/users_groups",
194 220 action="create", conditions=dict(method=["POST"]))
195 221 m.connect("users_groups", "/users_groups",
196 222 action="index", conditions=dict(method=["GET"]))
197 223 m.connect("formatted_users_groups", "/users_groups.{format}",
198 224 action="index", conditions=dict(method=["GET"]))
199 225 m.connect("new_users_group", "/users_groups/new",
200 226 action="new", conditions=dict(method=["GET"]))
201 227 m.connect("formatted_new_users_group", "/users_groups/new.{format}",
202 228 action="new", conditions=dict(method=["GET"]))
203 229 m.connect("update_users_group", "/users_groups/{id}",
204 230 action="update", conditions=dict(method=["PUT"]))
205 231 m.connect("delete_users_group", "/users_groups/{id}",
206 232 action="delete", conditions=dict(method=["DELETE"]))
207 233 m.connect("edit_users_group", "/users_groups/{id}/edit",
208 234 action="edit", conditions=dict(method=["GET"]))
209 235 m.connect("formatted_edit_users_group",
210 236 "/users_groups/{id}.{format}/edit",
211 237 action="edit", conditions=dict(method=["GET"]))
212 238 m.connect("users_group", "/users_groups/{id}",
213 239 action="show", conditions=dict(method=["GET"]))
214 240 m.connect("formatted_users_group", "/users_groups/{id}.{format}",
215 241 action="show", conditions=dict(method=["GET"]))
216 242
217 243 #EXTRAS USER ROUTES
218 244 m.connect("users_group_perm", "/users_groups_perm/{id}",
219 245 action="update_perm", conditions=dict(method=["PUT"]))
220 246
221 247 #ADMIN GROUP REST ROUTES
222 248 rmap.resource('group', 'groups',
223 249 controller='admin/groups', path_prefix=ADMIN_PREFIX)
224 250
225 251 #ADMIN PERMISSIONS REST ROUTES
226 252 rmap.resource('permission', 'permissions',
227 253 controller='admin/permissions', path_prefix=ADMIN_PREFIX)
228 254
229 255 ##ADMIN LDAP SETTINGS
230 256 rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
231 257 controller='admin/ldap_settings', action='ldap_settings',
232 258 conditions=dict(method=["POST"]))
233 259
234 260 rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
235 261 controller='admin/ldap_settings')
236 262
237 263 #ADMIN SETTINGS REST ROUTES
238 264 with rmap.submapper(path_prefix=ADMIN_PREFIX,
239 265 controller='admin/settings') as m:
240 266 m.connect("admin_settings", "/settings",
241 267 action="create", conditions=dict(method=["POST"]))
242 268 m.connect("admin_settings", "/settings",
243 269 action="index", conditions=dict(method=["GET"]))
244 270 m.connect("formatted_admin_settings", "/settings.{format}",
245 271 action="index", conditions=dict(method=["GET"]))
246 272 m.connect("admin_new_setting", "/settings/new",
247 273 action="new", conditions=dict(method=["GET"]))
248 274 m.connect("formatted_admin_new_setting", "/settings/new.{format}",
249 275 action="new", conditions=dict(method=["GET"]))
250 276 m.connect("/settings/{setting_id}",
251 277 action="update", conditions=dict(method=["PUT"]))
252 278 m.connect("/settings/{setting_id}",
253 279 action="delete", conditions=dict(method=["DELETE"]))
254 280 m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
255 281 action="edit", conditions=dict(method=["GET"]))
256 282 m.connect("formatted_admin_edit_setting",
257 283 "/settings/{setting_id}.{format}/edit",
258 284 action="edit", conditions=dict(method=["GET"]))
259 285 m.connect("admin_setting", "/settings/{setting_id}",
260 286 action="show", conditions=dict(method=["GET"]))
261 287 m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
262 288 action="show", conditions=dict(method=["GET"]))
263 289 m.connect("admin_settings_my_account", "/my_account",
264 290 action="my_account", conditions=dict(method=["GET"]))
265 291 m.connect("admin_settings_my_account_update", "/my_account_update",
266 292 action="my_account_update", conditions=dict(method=["PUT"]))
267 293 m.connect("admin_settings_create_repository", "/create_repository",
268 294 action="create_repository", conditions=dict(method=["GET"]))
269 295
296 #NOTIFICATION REST ROUTES
297 with rmap.submapper(path_prefix=ADMIN_PREFIX,
298 controller='admin/notifications') as m:
299 m.connect("notifications", "/notifications",
300 action="create", conditions=dict(method=["POST"]))
301 m.connect("notifications", "/notifications",
302 action="index", conditions=dict(method=["GET"]))
303 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
304 action="mark_all_read", conditions=dict(method=["GET"]))
305 m.connect("formatted_notifications", "/notifications.{format}",
306 action="index", conditions=dict(method=["GET"]))
307 m.connect("new_notification", "/notifications/new",
308 action="new", conditions=dict(method=["GET"]))
309 m.connect("formatted_new_notification", "/notifications/new.{format}",
310 action="new", conditions=dict(method=["GET"]))
311 m.connect("/notification/{notification_id}",
312 action="update", conditions=dict(method=["PUT"]))
313 m.connect("/notification/{notification_id}",
314 action="delete", conditions=dict(method=["DELETE"]))
315 m.connect("edit_notification", "/notification/{notification_id}/edit",
316 action="edit", conditions=dict(method=["GET"]))
317 m.connect("formatted_edit_notification",
318 "/notification/{notification_id}.{format}/edit",
319 action="edit", conditions=dict(method=["GET"]))
320 m.connect("notification", "/notification/{notification_id}",
321 action="show", conditions=dict(method=["GET"]))
322 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
323 action="show", conditions=dict(method=["GET"]))
270 324
271 325 #ADMIN MAIN PAGES
272 326 with rmap.submapper(path_prefix=ADMIN_PREFIX,
273 327 controller='admin/admin') as m:
274 328 m.connect('admin_home', '', action='index')
275 329 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
276 330 action='add_repo')
277 331
278 332 #==========================================================================
279 # API V1
333 # API V2
280 334 #==========================================================================
281 335 with rmap.submapper(path_prefix=ADMIN_PREFIX,
282 336 controller='api/api') as m:
283 337 m.connect('api', '/api')
284 338
285
286 339 #USER JOURNAL
287 340 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal')
288 341
289 342 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
290 343 controller='journal', action="public_journal")
291 344
292 345 rmap.connect('public_journal_rss', '%s/public_journal_rss' % ADMIN_PREFIX,
293 346 controller='journal', action="public_journal_rss")
294 347
295 348 rmap.connect('public_journal_atom',
296 349 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
297 350 action="public_journal_atom")
298 351
299 352 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
300 353 controller='journal', action='toggle_following',
301 354 conditions=dict(method=["POST"]))
302 355
303 356 #SEARCH
304 357 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
305 358 rmap.connect('search_repo', '%s/search/{search_repo:.*}' % ADMIN_PREFIX,
306 359 controller='search')
307 360
308 361 #LOGIN/LOGOUT/REGISTER/SIGN IN
309 362 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
310 363 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
311 364 action='logout')
312 365
313 366 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
314 367 action='register')
315 368
316 369 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
317 370 controller='login', action='password_reset')
318 371
319 372 rmap.connect('reset_password_confirmation',
320 373 '%s/password_reset_confirmation' % ADMIN_PREFIX,
321 374 controller='login', action='password_reset_confirmation')
322 375
323 376 #FEEDS
324 377 rmap.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
325 378 controller='feed', action='rss',
326 379 conditions=dict(function=check_repo))
327 380
328 381 rmap.connect('atom_feed_home', '/{repo_name:.*}/feed/atom',
329 382 controller='feed', action='atom',
330 383 conditions=dict(function=check_repo))
331 384
332 385 #==========================================================================
333 386 # REPOSITORY ROUTES
334 387 #==========================================================================
335 388 rmap.connect('summary_home', '/{repo_name:.*}',
336 389 controller='summary',
337 390 conditions=dict(function=check_repo))
338 391
339 392 rmap.connect('repos_group_home', '/{group_name:.*}',
340 393 controller='admin/repos_groups', action="show_by_name",
341 394 conditions=dict(function=check_group))
342 395
343 396 rmap.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
344 397 controller='changeset', revision='tip',
345 398 conditions=dict(function=check_repo))
346 399
400 rmap.connect('changeset_comment',
401 '/{repo_name:.*}/changeset/{revision}/comment',
402 controller='changeset', revision='tip', action='comment',
403 conditions=dict(function=check_repo))
404
405 rmap.connect('changeset_comment_delete',
406 '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
407 controller='changeset', action='delete_comment',
408 conditions=dict(function=check_repo, method=["DELETE"]))
409
347 410 rmap.connect('raw_changeset_home',
348 411 '/{repo_name:.*}/raw-changeset/{revision}',
349 412 controller='changeset', action='raw_changeset',
350 413 revision='tip', conditions=dict(function=check_repo))
351 414
352 415 rmap.connect('summary_home', '/{repo_name:.*}/summary',
353 416 controller='summary', conditions=dict(function=check_repo))
354 417
355 418 rmap.connect('shortlog_home', '/{repo_name:.*}/shortlog',
356 419 controller='shortlog', conditions=dict(function=check_repo))
357 420
358 421 rmap.connect('branches_home', '/{repo_name:.*}/branches',
359 422 controller='branches', conditions=dict(function=check_repo))
360 423
361 424 rmap.connect('tags_home', '/{repo_name:.*}/tags',
362 425 controller='tags', conditions=dict(function=check_repo))
363 426
427 rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks',
428 controller='bookmarks', conditions=dict(function=check_repo))
429
364 430 rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
365 431 controller='changelog', conditions=dict(function=check_repo))
366 432
367 433 rmap.connect('changelog_details', '/{repo_name:.*}/changelog_details/{cs}',
368 434 controller='changelog', action='changelog_details',
369 435 conditions=dict(function=check_repo))
370 436
371 437 rmap.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
372 438 controller='files', revision='tip', f_path='',
373 439 conditions=dict(function=check_repo))
374 440
375 441 rmap.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
376 442 controller='files', action='diff', revision='tip', f_path='',
377 443 conditions=dict(function=check_repo))
378 444
379 445 rmap.connect('files_rawfile_home',
380 446 '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
381 447 controller='files', action='rawfile', revision='tip',
382 448 f_path='', conditions=dict(function=check_repo))
383 449
384 450 rmap.connect('files_raw_home',
385 451 '/{repo_name:.*}/raw/{revision}/{f_path:.*}',
386 452 controller='files', action='raw', revision='tip', f_path='',
387 453 conditions=dict(function=check_repo))
388 454
389 455 rmap.connect('files_annotate_home',
390 456 '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
391 457 controller='files', action='annotate', revision='tip',
392 458 f_path='', conditions=dict(function=check_repo))
393 459
394 460 rmap.connect('files_edit_home',
395 461 '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
396 462 controller='files', action='edit', revision='tip',
397 463 f_path='', conditions=dict(function=check_repo))
398 464
399 465 rmap.connect('files_add_home',
400 466 '/{repo_name:.*}/add/{revision}/{f_path:.*}',
401 467 controller='files', action='add', revision='tip',
402 468 f_path='', conditions=dict(function=check_repo))
403 469
404 470 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
405 471 controller='files', action='archivefile',
406 472 conditions=dict(function=check_repo))
407 473
408 474 rmap.connect('files_nodelist_home',
409 475 '/{repo_name:.*}/nodelist/{revision}/{f_path:.*}',
410 476 controller='files', action='nodelist',
411 477 conditions=dict(function=check_repo))
412 478
413 479 rmap.connect('repo_settings_delete', '/{repo_name:.*}/settings',
414 480 controller='settings', action="delete",
415 481 conditions=dict(method=["DELETE"], function=check_repo))
416 482
417 483 rmap.connect('repo_settings_update', '/{repo_name:.*}/settings',
418 484 controller='settings', action="update",
419 485 conditions=dict(method=["PUT"], function=check_repo))
420 486
421 487 rmap.connect('repo_settings_home', '/{repo_name:.*}/settings',
422 488 controller='settings', action='index',
423 489 conditions=dict(function=check_repo))
424 490
425 491 rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
426 controller='settings', action='fork_create',
492 controller='forks', action='fork_create',
427 493 conditions=dict(function=check_repo, method=["POST"]))
428 494
429 495 rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
430 controller='settings', action='fork',
496 controller='forks', action='fork',
431 497 conditions=dict(function=check_repo))
432 498
499 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
500 controller='forks', action='forks',
501 conditions=dict(function=check_repo))
502
433 503 rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
434 504 controller='followers', action='followers',
435 505 conditions=dict(function=check_repo))
436 506
437 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
438 controller='forks', action='forks',
439 conditions=dict(function=check_repo))
440
441 507 return rmap
@@ -1,59 +1,59 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.admin
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Controller for Admin panel of Rhodecode
7 7
8 8 :created_on: Apr 7, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import request, tmpl_context as c
29 29 from sqlalchemy.orm import joinedload
30 30 from webhelpers.paginate import Page
31 31
32 32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 33 from rhodecode.lib.base import BaseController, render
34 34 from rhodecode.model.db import UserLog
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminController(BaseController):
40 40
41 41 @LoginRequired()
42 42 def __before__(self):
43 43 super(AdminController, self).__before__()
44 44
45 45 @HasPermissionAllDecorator('hg.admin')
46 46 def index(self):
47 47
48 48 users_log = self.sa.query(UserLog)\
49 49 .options(joinedload(UserLog.user))\
50 50 .options(joinedload(UserLog.repository))\
51 51 .order_by(UserLog.action_date.desc())
52 52
53 53 p = int(request.params.get('page', 1))
54 54 c.users_log = Page(users_log, page=p, items_per_page=10)
55 55 c.log_data = render('admin/admin_log.html')
56 56
57 57 if request.environ.get('HTTP_X_PARTIAL_XHR'):
58 58 return c.log_data
59 59 return render('admin/admin.html')
@@ -1,137 +1,137 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.ldap_settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 ldap controller for RhodeCode
7 7
8 8 :created_on: Nov 26, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import formencode
27 27 import traceback
28 28
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, response, session, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from sqlalchemy.exc import DatabaseError
36 36
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
40 40 from rhodecode.lib.exceptions import LdapImportError
41 41 from rhodecode.model.forms import LdapSettingsForm
42 from rhodecode.model.db import RhodeCodeSettings
42 from rhodecode.model.db import RhodeCodeSetting
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class LdapSettingsController(BaseController):
48 48
49 49 search_scope_choices = [('BASE', _('BASE'),),
50 50 ('ONELEVEL', _('ONELEVEL'),),
51 51 ('SUBTREE', _('SUBTREE'),),
52 52 ]
53 53 search_scope_default = 'SUBTREE'
54 54
55 55 tls_reqcert_choices = [('NEVER', _('NEVER'),),
56 56 ('ALLOW', _('ALLOW'),),
57 57 ('TRY', _('TRY'),),
58 58 ('DEMAND', _('DEMAND'),),
59 59 ('HARD', _('HARD'),),
60 60 ]
61 61 tls_reqcert_default = 'DEMAND'
62 62
63 63 tls_kind_choices = [('PLAIN', _('No encryption'),),
64 64 ('LDAPS', _('LDAPS connection'),),
65 65 ('START_TLS', _('START_TLS on LDAP connection'),)
66 66 ]
67 67
68 68 tls_kind_default = 'PLAIN'
69 69
70 70 @LoginRequired()
71 71 @HasPermissionAllDecorator('hg.admin')
72 72 def __before__(self):
73 73 c.admin_user = session.get('admin_user')
74 74 c.admin_username = session.get('admin_username')
75 75 c.search_scope_choices = self.search_scope_choices
76 76 c.tls_reqcert_choices = self.tls_reqcert_choices
77 77 c.tls_kind_choices = self.tls_kind_choices
78 78
79 79 c.search_scope_cur = self.search_scope_default
80 80 c.tls_reqcert_cur = self.tls_reqcert_default
81 81 c.tls_kind_cur = self.tls_kind_default
82 82
83 83 super(LdapSettingsController, self).__before__()
84 84
85 85 def index(self):
86 defaults = RhodeCodeSettings.get_ldap_settings()
86 defaults = RhodeCodeSetting.get_ldap_settings()
87 87 c.search_scope_cur = defaults.get('ldap_search_scope')
88 88 c.tls_reqcert_cur = defaults.get('ldap_tls_reqcert')
89 89 c.tls_kind_cur = defaults.get('ldap_tls_kind')
90 90
91 91 return htmlfill.render(
92 92 render('admin/ldap/ldap.html'),
93 93 defaults=defaults,
94 94 encoding="UTF-8",
95 95 force_defaults=True,)
96 96
97 97 def ldap_settings(self):
98 98 """POST ldap create and store ldap settings"""
99 99
100 100 _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices],
101 101 [x[0] for x in self.search_scope_choices],
102 102 [x[0] for x in self.tls_kind_choices])()
103 103
104 104 try:
105 105 form_result = _form.to_python(dict(request.POST))
106 106 try:
107 107
108 108 for k, v in form_result.items():
109 109 if k.startswith('ldap_'):
110 setting = RhodeCodeSettings.get_by_name(k)
110 setting = RhodeCodeSetting.get_by_name(k)
111 111 setting.app_settings_value = v
112 112 self.sa.add(setting)
113 113
114 114 self.sa.commit()
115 115 h.flash(_('Ldap settings updated successfully'),
116 116 category='success')
117 117 except (DatabaseError,):
118 118 raise
119 119 except LdapImportError:
120 120 h.flash(_('Unable to activate ldap. The "python-ldap" library '
121 121 'is missing.'), category='warning')
122 122
123 123 except formencode.Invalid, errors:
124 124 e = errors.error_dict or {}
125 125
126 126 return htmlfill.render(
127 127 render('admin/ldap/ldap.html'),
128 128 defaults=errors.value,
129 129 errors=e,
130 130 prefix_error=False,
131 131 encoding="UTF-8")
132 132 except Exception:
133 133 log.error(traceback.format_exc())
134 134 h.flash(_('error occurred during update of ldap settings'),
135 135 category='error')
136 136
137 137 return redirect(url('ldap_home'))
@@ -1,167 +1,169 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.permissions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 permissions controller for Rhodecode
7 7
8 8 :created_on: Apr 27, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, session, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.model.forms import DefaultPermissionsForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.db import User
41 from rhodecode.model.meta import Session
41 42
42 43 log = logging.getLogger(__name__)
43 44
44 45
45 46 class PermissionsController(BaseController):
46 47 """REST Controller styled on the Atom Publishing Protocol"""
47 48 # To properly map this controller, ensure your config/routing.py
48 49 # file has a resource setup:
49 50 # map.resource('permission', 'permissions')
50 51
51 52 @LoginRequired()
52 53 @HasPermissionAllDecorator('hg.admin')
53 54 def __before__(self):
54 55 c.admin_user = session.get('admin_user')
55 56 c.admin_username = session.get('admin_username')
56 57 super(PermissionsController, self).__before__()
57 58
58 59 self.perms_choices = [('repository.none', _('None'),),
59 60 ('repository.read', _('Read'),),
60 61 ('repository.write', _('Write'),),
61 62 ('repository.admin', _('Admin'),)]
62 63 self.register_choices = [
63 64 ('hg.register.none',
64 65 _('disabled')),
65 66 ('hg.register.manual_activate',
66 67 _('allowed with manual account activation')),
67 68 ('hg.register.auto_activate',
68 69 _('allowed with automatic account activation')), ]
69 70
70 71 self.create_choices = [('hg.create.none', _('Disabled')),
71 72 ('hg.create.repository', _('Enabled'))]
72 73
73 74 def index(self, format='html'):
74 75 """GET /permissions: All items in the collection"""
75 76 # url('permissions')
76 77
77 78 def create(self):
78 79 """POST /permissions: Create a new item"""
79 80 # url('permissions')
80 81
81 82 def new(self, format='html'):
82 83 """GET /permissions/new: Form to create a new item"""
83 84 # url('new_permission')
84 85
85 86 def update(self, id):
86 87 """PUT /permissions/id: Update an existing item"""
87 88 # Forms posted to this method should contain a hidden field:
88 89 # <input type="hidden" name="_method" value="PUT" />
89 90 # Or using helpers:
90 91 # h.form(url('permission', id=ID),
91 92 # method='put')
92 93 # url('permission', id=ID)
93 94
94 95 permission_model = PermissionModel()
95 96
96 97 _form = DefaultPermissionsForm([x[0] for x in self.perms_choices],
97 98 [x[0] for x in self.register_choices],
98 99 [x[0] for x in self.create_choices])()
99 100
100 101 try:
101 102 form_result = _form.to_python(dict(request.POST))
102 103 form_result.update({'perm_user_name': id})
103 104 permission_model.update(form_result)
105 Session.commit()
104 106 h.flash(_('Default permissions updated successfully'),
105 107 category='success')
106 108
107 109 except formencode.Invalid, errors:
108 110 c.perms_choices = self.perms_choices
109 111 c.register_choices = self.register_choices
110 112 c.create_choices = self.create_choices
111 113 defaults = errors.value
112 114
113 115 return htmlfill.render(
114 116 render('admin/permissions/permissions.html'),
115 117 defaults=defaults,
116 118 errors=errors.error_dict or {},
117 119 prefix_error=False,
118 120 encoding="UTF-8")
119 121 except Exception:
120 122 log.error(traceback.format_exc())
121 123 h.flash(_('error occurred during update of permissions'),
122 124 category='error')
123 125
124 126 return redirect(url('edit_permission', id=id))
125 127
126 128 def delete(self, id):
127 129 """DELETE /permissions/id: Delete an existing item"""
128 130 # Forms posted to this method should contain a hidden field:
129 131 # <input type="hidden" name="_method" value="DELETE" />
130 132 # Or using helpers:
131 133 # h.form(url('permission', id=ID),
132 134 # method='delete')
133 135 # url('permission', id=ID)
134 136
135 137 def show(self, id, format='html'):
136 138 """GET /permissions/id: Show a specific item"""
137 139 # url('permission', id=ID)
138 140
139 141 def edit(self, id, format='html'):
140 142 """GET /permissions/id/edit: Form to edit an existing item"""
141 143 #url('edit_permission', id=ID)
142 144 c.perms_choices = self.perms_choices
143 145 c.register_choices = self.register_choices
144 146 c.create_choices = self.create_choices
145 147
146 148 if id == 'default':
147 149 default_user = User.get_by_username('default')
148 150 defaults = {'_method': 'put',
149 151 'anonymous': default_user.active}
150 152
151 153 for p in default_user.user_perms:
152 154 if p.permission.permission_name.startswith('repository.'):
153 155 defaults['default_perm'] = p.permission.permission_name
154 156
155 157 if p.permission.permission_name.startswith('hg.register.'):
156 158 defaults['default_register'] = p.permission.permission_name
157 159
158 160 if p.permission.permission_name.startswith('hg.create.'):
159 161 defaults['default_create'] = p.permission.permission_name
160 162
161 163 return htmlfill.render(
162 164 render('admin/permissions/permissions.html'),
163 165 defaults=defaults,
164 166 encoding="UTF-8",
165 167 force_defaults=True,)
166 168 else:
167 169 return redirect(url('admin_home'))
@@ -1,398 +1,433 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.repos
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Admin controller for RhodeCode
6 Repositories controller for RhodeCode
7 7
8 8 :created_on: Apr 7, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 from formencode import htmlfill
30 30
31 31 from paste.httpexceptions import HTTPInternalServerError
32 from pylons import request, response, session, tmpl_context as c, url
33 from pylons.controllers.util import abort, redirect
32 from pylons import request, session, tmpl_context as c, url
33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 from sqlalchemy.exc import IntegrityError
35 36
36 37 from rhodecode.lib import helpers as h
37 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
38 39 HasPermissionAnyDecorator, HasRepoPermissionAllDecorator
39 40 from rhodecode.lib.base import BaseController, render
40 41 from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
41 42 from rhodecode.lib.helpers import get_token
42 from rhodecode.model.db import User, Repository, UserFollowing, Group
43 from rhodecode.model.meta import Session
44 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup
43 45 from rhodecode.model.forms import RepoForm
44 46 from rhodecode.model.scm import ScmModel
45 47 from rhodecode.model.repo import RepoModel
46 from sqlalchemy.exc import IntegrityError
47 48
48 49 log = logging.getLogger(__name__)
49 50
50 51
51 52 class ReposController(BaseController):
52 53 """
53 54 REST Controller styled on the Atom Publishing Protocol"""
54 55 # To properly map this controller, ensure your config/routing.py
55 56 # file has a resource setup:
56 57 # map.resource('repo', 'repos')
57 58
58 59 @LoginRequired()
59 60 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
60 61 def __before__(self):
61 62 c.admin_user = session.get('admin_user')
62 63 c.admin_username = session.get('admin_username')
63 64 super(ReposController, self).__before__()
64 65
65 66 def __load_defaults(self):
66 c.repo_groups = Group.groups_choices()
67 c.repo_groups = RepoGroup.groups_choices()
67 68 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
68
69
69 70 repo_model = RepoModel()
70 71 c.users_array = repo_model.get_users_js()
71 72 c.users_groups_array = repo_model.get_users_groups_js()
72 73
73 74 def __load_data(self, repo_name=None):
74 75 """
75 76 Load defaults settings for edit, and update
76 77
77 78 :param repo_name:
78 79 """
79 80 self.__load_defaults()
80 81
81 82 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
82 83 repo = db_repo.scm_instance
83 84
84 85 if c.repo_info is None:
85 86 h.flash(_('%s repository is not mapped to db perhaps'
86 87 ' it was created or renamed from the filesystem'
87 88 ' please run the application again'
88 89 ' in order to rescan repositories') % repo_name,
89 90 category='error')
90 91
91 92 return redirect(url('repos'))
92 93
93 94 c.default_user_id = User.get_by_username('default').user_id
94 95 c.in_public_journal = UserFollowing.query()\
95 96 .filter(UserFollowing.user_id == c.default_user_id)\
96 97 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
97 98
98 99 if c.repo_info.stats:
99 last_rev = c.repo_info.stats.stat_on_revision
100 # this is on what revision we ended up so we add +1 for count
101 last_rev = c.repo_info.stats.stat_on_revision + 1
100 102 else:
101 103 last_rev = 0
102 104 c.stats_revision = last_rev
103 105
104 c.repo_last_rev = repo.count() - 1 if repo.revisions else 0
106 c.repo_last_rev = repo.count() if repo.revisions else 0
105 107
106 108 if last_rev == 0 or c.repo_last_rev == 0:
107 109 c.stats_percentage = 0
108 110 else:
109 111 c.stats_percentage = '%.2f' % ((float((last_rev)) /
110 112 c.repo_last_rev) * 100)
111 113
112 114 defaults = RepoModel()._get_defaults(repo_name)
115
116 c.repos_list = [('', _('--REMOVE FORK--'))]
117 c.repos_list += [(x.repo_id, x.repo_name) for x in
118 Repository.query().order_by(Repository.repo_name).all()]
113 119 return defaults
114 120
115 121 @HasPermissionAllDecorator('hg.admin')
116 122 def index(self, format='html'):
117 123 """GET /repos: All items in the collection"""
118 124 # url('repos')
119 125
120 126 c.repos_list = ScmModel().get_repos(Repository.query()
121 127 .order_by(Repository.repo_name)
122 128 .all(), sort_key='name_sort')
123 129 return render('admin/repos/repos.html')
124 130
125 131 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
126 132 def create(self):
127 133 """
128 134 POST /repos: Create a new item"""
129 135 # url('repos')
130 repo_model = RepoModel()
136
131 137 self.__load_defaults()
132 138 form_result = {}
133 139 try:
134 140 form_result = RepoForm(repo_groups=c.repo_groups_choices)()\
135 141 .to_python(dict(request.POST))
136 repo_model.create(form_result, self.rhodecode_user)
142 RepoModel().create(form_result, self.rhodecode_user)
137 143 if form_result['clone_uri']:
138 144 h.flash(_('created repository %s from %s') \
139 145 % (form_result['repo_name'], form_result['clone_uri']),
140 146 category='success')
141 147 else:
142 148 h.flash(_('created repository %s') % form_result['repo_name'],
143 149 category='success')
144 150
145 151 if request.POST.get('user_created'):
146 #created by regular non admin user
152 # created by regular non admin user
147 153 action_logger(self.rhodecode_user, 'user_created_repo',
148 154 form_result['repo_name_full'], '', self.sa)
149 155 else:
150 156 action_logger(self.rhodecode_user, 'admin_created_repo',
151 157 form_result['repo_name_full'], '', self.sa)
152
158 Session.commit()
153 159 except formencode.Invalid, errors:
154 160
155 161 c.new_repo = errors.value['repo_name']
156 162
157 163 if request.POST.get('user_created'):
158 164 r = render('admin/repos/repo_add_create_repository.html')
159 165 else:
160 166 r = render('admin/repos/repo_add.html')
161 167
162 168 return htmlfill.render(
163 169 r,
164 170 defaults=errors.value,
165 171 errors=errors.error_dict or {},
166 172 prefix_error=False,
167 173 encoding="UTF-8")
168 174
169 175 except Exception:
170 176 log.error(traceback.format_exc())
171 177 msg = _('error occurred during creation of repository %s') \
172 178 % form_result.get('repo_name')
173 179 h.flash(msg, category='error')
174 180 if request.POST.get('user_created'):
175 181 return redirect(url('home'))
176 182 return redirect(url('repos'))
177 183
178 184 @HasPermissionAllDecorator('hg.admin')
179 185 def new(self, format='html'):
180 186 """GET /repos/new: Form to create a new item"""
181 187 new_repo = request.GET.get('repo', '')
182 188 c.new_repo = repo_name_slug(new_repo)
183 189 self.__load_defaults()
184 190 return render('admin/repos/repo_add.html')
185 191
186 192 @HasPermissionAllDecorator('hg.admin')
187 193 def update(self, repo_name):
188 194 """
189 195 PUT /repos/repo_name: Update an existing item"""
190 196 # Forms posted to this method should contain a hidden field:
191 197 # <input type="hidden" name="_method" value="PUT" />
192 198 # Or using helpers:
193 199 # h.form(url('repo', repo_name=ID),
194 200 # method='put')
195 201 # url('repo', repo_name=ID)
196 202 self.__load_defaults()
197 203 repo_model = RepoModel()
198 204 changed_name = repo_name
199 205 _form = RepoForm(edit=True, old_data={'repo_name': repo_name},
200 206 repo_groups=c.repo_groups_choices)()
201 207 try:
202 208 form_result = _form.to_python(dict(request.POST))
203 209 repo = repo_model.update(repo_name, form_result)
204 210 invalidate_cache('get_repo_cached_%s' % repo_name)
205 211 h.flash(_('Repository %s updated successfully' % repo_name),
206 212 category='success')
207 213 changed_name = repo.repo_name
208 214 action_logger(self.rhodecode_user, 'admin_updated_repo',
209 215 changed_name, '', self.sa)
210
216 Session.commit()
211 217 except formencode.Invalid, errors:
212 218 defaults = self.__load_data(repo_name)
213 219 defaults.update(errors.value)
214 220 return htmlfill.render(
215 221 render('admin/repos/repo_edit.html'),
216 222 defaults=defaults,
217 223 errors=errors.error_dict or {},
218 224 prefix_error=False,
219 225 encoding="UTF-8")
220 226
221 227 except Exception:
222 228 log.error(traceback.format_exc())
223 229 h.flash(_('error occurred during update of repository %s') \
224 230 % repo_name, category='error')
225 231 return redirect(url('edit_repo', repo_name=changed_name))
226 232
227 233 @HasPermissionAllDecorator('hg.admin')
228 234 def delete(self, repo_name):
229 235 """
230 236 DELETE /repos/repo_name: Delete an existing item"""
231 237 # Forms posted to this method should contain a hidden field:
232 238 # <input type="hidden" name="_method" value="DELETE" />
233 239 # Or using helpers:
234 240 # h.form(url('repo', repo_name=ID),
235 241 # method='delete')
236 242 # url('repo', repo_name=ID)
237 243
238 244 repo_model = RepoModel()
239 245 repo = repo_model.get_by_repo_name(repo_name)
240 246 if not repo:
241 247 h.flash(_('%s repository is not mapped to db perhaps'
242 248 ' it was moved or renamed from the filesystem'
243 249 ' please run the application again'
244 250 ' in order to rescan repositories') % repo_name,
245 251 category='error')
246 252
247 253 return redirect(url('repos'))
248 254 try:
249 255 action_logger(self.rhodecode_user, 'admin_deleted_repo',
250 256 repo_name, '', self.sa)
251 257 repo_model.delete(repo)
252 258 invalidate_cache('get_repo_cached_%s' % repo_name)
253 259 h.flash(_('deleted repository %s') % repo_name, category='success')
254
260 Session.commit()
255 261 except IntegrityError, e:
256 if e.message.find('repositories_fork_id_fkey'):
262 if e.message.find('repositories_fork_id_fkey') != -1:
257 263 log.error(traceback.format_exc())
258 264 h.flash(_('Cannot delete %s it still contains attached '
259 265 'forks') % repo_name,
260 266 category='warning')
261 267 else:
262 268 log.error(traceback.format_exc())
263 269 h.flash(_('An error occurred during '
264 270 'deletion of %s') % repo_name,
265 271 category='error')
266 272
267 273 except Exception, e:
268 274 log.error(traceback.format_exc())
269 275 h.flash(_('An error occurred during deletion of %s') % repo_name,
270 276 category='error')
271 277
272 278 return redirect(url('repos'))
273 279
274
275 @HasRepoPermissionAllDecorator('repository.admin')
280 @HasRepoPermissionAllDecorator('repository.admin')
276 281 def delete_perm_user(self, repo_name):
277 282 """
278 283 DELETE an existing repository permission user
279 284
280 285 :param repo_name:
281 286 """
282 287
283 288 try:
284 repo_model = RepoModel()
285 repo_model.delete_perm_user(request.POST, repo_name)
286 except Exception, e:
289 RepoModel().revoke_user_permission(repo=repo_name,
290 user=request.POST['user_id'])
291 Session.commit()
292 except Exception:
293 log.error(traceback.format_exc())
287 294 h.flash(_('An error occurred during deletion of repository user'),
288 295 category='error')
289 296 raise HTTPInternalServerError()
290 297
291 298 @HasRepoPermissionAllDecorator('repository.admin')
292 299 def delete_perm_users_group(self, repo_name):
293 300 """
294 301 DELETE an existing repository permission users group
295 302
296 303 :param repo_name:
297 304 """
305
298 306 try:
299 repo_model = RepoModel()
300 repo_model.delete_perm_users_group(request.POST, repo_name)
301 except Exception, e:
307 RepoModel().revoke_users_group_permission(
308 repo=repo_name, group_name=request.POST['users_group_id']
309 )
310 Session.commit()
311 except Exception:
312 log.error(traceback.format_exc())
302 313 h.flash(_('An error occurred during deletion of repository'
303 314 ' users groups'),
304 315 category='error')
305 316 raise HTTPInternalServerError()
306 317
307 318 @HasPermissionAllDecorator('hg.admin')
308 319 def repo_stats(self, repo_name):
309 320 """
310 321 DELETE an existing repository statistics
311 322
312 323 :param repo_name:
313 324 """
314 325
315 326 try:
316 repo_model = RepoModel()
317 repo_model.delete_stats(repo_name)
327 RepoModel().delete_stats(repo_name)
328 Session.commit()
318 329 except Exception, e:
319 330 h.flash(_('An error occurred during deletion of repository stats'),
320 331 category='error')
321 332 return redirect(url('edit_repo', repo_name=repo_name))
322 333
323 334 @HasPermissionAllDecorator('hg.admin')
324 335 def repo_cache(self, repo_name):
325 336 """
326 337 INVALIDATE existing repository cache
327 338
328 339 :param repo_name:
329 340 """
330 341
331 342 try:
332 343 ScmModel().mark_for_invalidation(repo_name)
344 Session.commit()
333 345 except Exception, e:
334 346 h.flash(_('An error occurred during cache invalidation'),
335 347 category='error')
336 348 return redirect(url('edit_repo', repo_name=repo_name))
337 349
338 350 @HasPermissionAllDecorator('hg.admin')
339 351 def repo_public_journal(self, repo_name):
340 352 """
341 353 Set's this repository to be visible in public journal,
342 354 in other words assing default user to follow this repo
343 355
344 356 :param repo_name:
345 357 """
346 358
347 359 cur_token = request.POST.get('auth_token')
348 360 token = get_token()
349 361 if cur_token == token:
350 362 try:
351 363 repo_id = Repository.get_by_repo_name(repo_name).repo_id
352 364 user_id = User.get_by_username('default').user_id
353 365 self.scm_model.toggle_following_repo(repo_id, user_id)
354 366 h.flash(_('Updated repository visibility in public journal'),
355 367 category='success')
368 Session.commit()
356 369 except:
357 370 h.flash(_('An error occurred during setting this'
358 371 ' repository in public journal'),
359 372 category='error')
360 373
361 374 else:
362 375 h.flash(_('Token mismatch'), category='error')
363 376 return redirect(url('edit_repo', repo_name=repo_name))
364 377
365 378 @HasPermissionAllDecorator('hg.admin')
366 379 def repo_pull(self, repo_name):
367 380 """
368 381 Runs task to update given repository with remote changes,
369 382 ie. make pull on remote location
370 383
371 384 :param repo_name:
372 385 """
373 386 try:
374 387 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
375 388 h.flash(_('Pulled from remote location'), category='success')
376 389 except Exception, e:
377 390 h.flash(_('An error occurred during pull from remote location'),
378 391 category='error')
379 392
380 393 return redirect(url('edit_repo', repo_name=repo_name))
381 394
382 395 @HasPermissionAllDecorator('hg.admin')
396 def repo_as_fork(self, repo_name):
397 """
398 Mark given repository as a fork of another
399
400 :param repo_name:
401 """
402 try:
403 fork_id = request.POST.get('id_fork_of')
404 repo = ScmModel().mark_as_fork(repo_name, fork_id,
405 self.rhodecode_user.username)
406 fork = repo.fork.repo_name if repo.fork else _('Nothing')
407 Session.commit()
408 h.flash(_('Marked repo %s as fork of %s' % (repo_name,fork)),
409 category='success')
410 except Exception, e:
411 raise
412 h.flash(_('An error occurred during this operation'),
413 category='error')
414
415 return redirect(url('edit_repo', repo_name=repo_name))
416
417 @HasPermissionAllDecorator('hg.admin')
383 418 def show(self, repo_name, format='html'):
384 419 """GET /repos/repo_name: Show a specific item"""
385 420 # url('repo', repo_name=ID)
386 421
387 422 @HasPermissionAllDecorator('hg.admin')
388 423 def edit(self, repo_name, format='html'):
389 424 """GET /repos/repo_name/edit: Form to edit an existing item"""
390 425 # url('edit_repo', repo_name=ID)
391 426 defaults = self.__load_data(repo_name)
392 427
393 428 return htmlfill.render(
394 429 render('admin/repos/repo_edit.html'),
395 430 defaults=defaults,
396 431 encoding="UTF-8",
397 432 force_defaults=False
398 433 )
@@ -1,229 +1,313 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.admin.repos_groups
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Repositories groups controller for RhodeCode
7
8 :created_on: Mar 23, 2010
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
1 26 import logging
2 27 import traceback
3 28 import formencode
4 29
5 30 from formencode import htmlfill
6 from operator import itemgetter
7 31
8 from pylons import request, response, session, tmpl_context as c, url
9 from pylons.controllers.util import abort, redirect
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
10 34 from pylons.i18n.translation import _
11 35
12 36 from sqlalchemy.exc import IntegrityError
13 37
14 38 from rhodecode.lib import helpers as h
15 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator
39 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\
40 HasReposGroupPermissionAnyDecorator
16 41 from rhodecode.lib.base import BaseController, render
17 from rhodecode.model.db import Group
42 from rhodecode.model.db import RepoGroup
18 43 from rhodecode.model.repos_group import ReposGroupModel
19 44 from rhodecode.model.forms import ReposGroupForm
45 from rhodecode.model.meta import Session
46 from rhodecode.model.repo import RepoModel
47 from webob.exc import HTTPInternalServerError
20 48
21 49 log = logging.getLogger(__name__)
22 50
23 51
24 52 class ReposGroupsController(BaseController):
25 53 """REST Controller styled on the Atom Publishing Protocol"""
26 54 # To properly map this controller, ensure your config/routing.py
27 55 # file has a resource setup:
28 56 # map.resource('repos_group', 'repos_groups')
29 57
30 58 @LoginRequired()
31 59 def __before__(self):
32 60 super(ReposGroupsController, self).__before__()
33 61
34 62 def __load_defaults(self):
35 c.repo_groups = Group.groups_choices()
63 c.repo_groups = RepoGroup.groups_choices()
36 64 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
37 65
66 repo_model = RepoModel()
67 c.users_array = repo_model.get_users_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
69
38 70 def __load_data(self, group_id):
39 71 """
40 72 Load defaults settings for edit, and update
41 73
42 74 :param group_id:
43 75 """
44 76 self.__load_defaults()
45 77
46 repo_group = Group.get(group_id)
78 repo_group = RepoGroup.get(group_id)
47 79
48 80 data = repo_group.get_dict()
49 81
50 82 data['group_name'] = repo_group.name
51 83
84 # fill repository users
85 for p in repo_group.repo_group_to_perm:
86 data.update({'u_perm_%s' % p.user.username:
87 p.permission.permission_name})
88
89 # fill repository groups
90 for p in repo_group.users_group_to_perm:
91 data.update({'g_perm_%s' % p.users_group.users_group_name:
92 p.permission.permission_name})
93
52 94 return data
53 95
54 96 @HasPermissionAnyDecorator('hg.admin')
55 97 def index(self, format='html'):
56 98 """GET /repos_groups: All items in the collection"""
57 99 # url('repos_groups')
58
59 sk = lambda g:g.parents[0].group_name if g.parents else g.group_name
60 c.groups = sorted(Group.query().all(), key=sk)
100 sk = lambda g: g.parents[0].group_name if g.parents else g.group_name
101 c.groups = sorted(RepoGroup.query().all(), key=sk)
61 102 return render('admin/repos_groups/repos_groups_show.html')
62 103
63 104 @HasPermissionAnyDecorator('hg.admin')
64 105 def create(self):
65 106 """POST /repos_groups: Create a new item"""
66 107 # url('repos_groups')
67 108 self.__load_defaults()
68 repos_group_model = ReposGroupModel()
69 repos_group_form = ReposGroupForm(available_groups=
109 repos_group_form = ReposGroupForm(available_groups =
70 110 c.repo_groups_choices)()
71 111 try:
72 112 form_result = repos_group_form.to_python(dict(request.POST))
73 repos_group_model.create(form_result)
113 ReposGroupModel().create(
114 group_name=form_result['group_name'],
115 group_description=form_result['group_description'],
116 parent=form_result['group_parent_id']
117 )
118 Session.commit()
74 119 h.flash(_('created repos group %s') \
75 120 % form_result['group_name'], category='success')
76 121 #TODO: in futureaction_logger(, '', '', '', self.sa)
77 122 except formencode.Invalid, errors:
78 123
79 124 return htmlfill.render(
80 125 render('admin/repos_groups/repos_groups_add.html'),
81 126 defaults=errors.value,
82 127 errors=errors.error_dict or {},
83 128 prefix_error=False,
84 129 encoding="UTF-8")
85 130 except Exception:
86 131 log.error(traceback.format_exc())
87 132 h.flash(_('error occurred during creation of repos group %s') \
88 133 % request.POST.get('group_name'), category='error')
89 134
90 135 return redirect(url('repos_groups'))
91 136
92
93 137 @HasPermissionAnyDecorator('hg.admin')
94 138 def new(self, format='html'):
95 139 """GET /repos_groups/new: Form to create a new item"""
96 140 # url('new_repos_group')
97 141 self.__load_defaults()
98 142 return render('admin/repos_groups/repos_groups_add.html')
99 143
100 144 @HasPermissionAnyDecorator('hg.admin')
101 145 def update(self, id):
102 146 """PUT /repos_groups/id: Update an existing item"""
103 147 # Forms posted to this method should contain a hidden field:
104 148 # <input type="hidden" name="_method" value="PUT" />
105 149 # Or using helpers:
106 150 # h.form(url('repos_group', id=ID),
107 151 # method='put')
108 152 # url('repos_group', id=ID)
109 153
110 154 self.__load_defaults()
111 c.repos_group = Group.get(id)
155 c.repos_group = RepoGroup.get(id)
112 156
113 repos_group_model = ReposGroupModel()
114 repos_group_form = ReposGroupForm(edit=True,
115 old_data=c.repos_group.get_dict(),
116 available_groups=
117 c.repo_groups_choices)()
157 repos_group_form = ReposGroupForm(
158 edit=True,
159 old_data=c.repos_group.get_dict(),
160 available_groups=c.repo_groups_choices
161 )()
118 162 try:
119 163 form_result = repos_group_form.to_python(dict(request.POST))
120 repos_group_model.update(id, form_result)
164 ReposGroupModel().update(id, form_result)
165 Session.commit()
121 166 h.flash(_('updated repos group %s') \
122 167 % form_result['group_name'], category='success')
123 168 #TODO: in futureaction_logger(, '', '', '', self.sa)
124 169 except formencode.Invalid, errors:
125 170
126 171 return htmlfill.render(
127 172 render('admin/repos_groups/repos_groups_edit.html'),
128 173 defaults=errors.value,
129 174 errors=errors.error_dict or {},
130 175 prefix_error=False,
131 176 encoding="UTF-8")
132 177 except Exception:
133 178 log.error(traceback.format_exc())
134 179 h.flash(_('error occurred during update of repos group %s') \
135 180 % request.POST.get('group_name'), category='error')
136 181
137 182 return redirect(url('repos_groups'))
138 183
139
140 184 @HasPermissionAnyDecorator('hg.admin')
141 185 def delete(self, id):
142 186 """DELETE /repos_groups/id: Delete an existing item"""
143 187 # Forms posted to this method should contain a hidden field:
144 188 # <input type="hidden" name="_method" value="DELETE" />
145 189 # Or using helpers:
146 190 # h.form(url('repos_group', id=ID),
147 191 # method='delete')
148 192 # url('repos_group', id=ID)
149 193
150 repos_group_model = ReposGroupModel()
151 gr = Group.get(id)
194 gr = RepoGroup.get(id)
152 195 repos = gr.repositories.all()
153 196 if repos:
154 197 h.flash(_('This group contains %s repositores and cannot be '
155 198 'deleted' % len(repos)),
156 199 category='error')
157 200 return redirect(url('repos_groups'))
158 201
159 202 try:
160 repos_group_model.delete(id)
203 ReposGroupModel().delete(id)
204 Session.commit()
161 205 h.flash(_('removed repos group %s' % gr.group_name), category='success')
162 206 #TODO: in future action_logger(, '', '', '', self.sa)
163 207 except IntegrityError, e:
164 if e.message.find('groups_group_parent_id_fkey'):
208 if e.message.find('groups_group_parent_id_fkey') != -1:
165 209 log.error(traceback.format_exc())
166 210 h.flash(_('Cannot delete this group it still contains '
167 211 'subgroups'),
168 212 category='warning')
169 213 else:
170 214 log.error(traceback.format_exc())
171 215 h.flash(_('error occurred during deletion of repos '
172 216 'group %s' % gr.group_name), category='error')
173 217
174 218 except Exception:
175 219 log.error(traceback.format_exc())
176 220 h.flash(_('error occurred during deletion of repos '
177 221 'group %s' % gr.group_name), category='error')
178 222
179 223 return redirect(url('repos_groups'))
180 224
225 @HasReposGroupPermissionAnyDecorator('group.admin')
226 def delete_repos_group_user_perm(self, group_name):
227 """
228 DELETE an existing repositories group permission user
229
230 :param group_name:
231 """
232
233 try:
234 ReposGroupModel().revoke_user_permission(
235 repos_group=group_name, user=request.POST['user_id']
236 )
237 Session.commit()
238 except Exception:
239 log.error(traceback.format_exc())
240 h.flash(_('An error occurred during deletion of group user'),
241 category='error')
242 raise HTTPInternalServerError()
243
244 @HasReposGroupPermissionAnyDecorator('group.admin')
245 def delete_repos_group_users_group_perm(self, group_name):
246 """
247 DELETE an existing repositories group permission users group
248
249 :param group_name:
250 """
251
252 try:
253 ReposGroupModel().revoke_users_group_permission(
254 repos_group=group_name,
255 group_name=request.POST['users_group_id']
256 )
257 Session.commit()
258 except Exception:
259 log.error(traceback.format_exc())
260 h.flash(_('An error occurred during deletion of group'
261 ' users groups'),
262 category='error')
263 raise HTTPInternalServerError()
264
181 265 def show_by_name(self, group_name):
182 id_ = Group.get_by_group_name(group_name).group_id
266 id_ = RepoGroup.get_by_group_name(group_name).group_id
183 267 return self.show(id_)
184 268
269 @HasReposGroupPermissionAnyDecorator('group.read', 'group.write',
270 'group.admin')
185 271 def show(self, id, format='html'):
186 272 """GET /repos_groups/id: Show a specific item"""
187 273 # url('repos_group', id=ID)
188 274
189 c.group = Group.get(id)
275 c.group = RepoGroup.get(id)
190 276
191 277 if c.group:
192 278 c.group_repos = c.group.repositories.all()
193 279 else:
194 280 return redirect(url('home'))
195 281
196 282 #overwrite our cached list with current filter
197 283 gr_filter = c.group_repos
198 284 c.cached_repo_list = self.scm_model.get_repos(all_repos=gr_filter)
199 285
200 286 c.repos_list = c.cached_repo_list
201 287
202 288 c.repo_cnt = 0
203 289
204 c.groups = self.sa.query(Group).order_by(Group.group_name)\
205 .filter(Group.group_parent_id == id).all()
290 c.groups = self.sa.query(RepoGroup).order_by(RepoGroup.group_name)\
291 .filter(RepoGroup.group_parent_id == id).all()
206 292
207 293 return render('admin/repos_groups/repos_groups.html')
208 294
209 295 @HasPermissionAnyDecorator('hg.admin')
210 296 def edit(self, id, format='html'):
211 297 """GET /repos_groups/id/edit: Form to edit an existing item"""
212 298 # url('edit_repos_group', id=ID)
213 299
214 300 id_ = int(id)
215 301
216 c.repos_group = Group.get(id_)
302 c.repos_group = RepoGroup.get(id_)
217 303 defaults = self.__load_data(id_)
218 304
219 305 # we need to exclude this group from the group list for editing
220 c.repo_groups = filter(lambda x:x[0] != id_, c.repo_groups)
306 c.repo_groups = filter(lambda x: x[0] != id_, c.repo_groups)
221 307
222 308 return htmlfill.render(
223 309 render('admin/repos_groups/repos_groups_edit.html'),
224 310 defaults=defaults,
225 311 encoding="UTF-8",
226 312 force_defaults=False
227 313 )
228
229
@@ -1,397 +1,414 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 settings controller for rhodecode admin
7 7
8 8 :created_on: Jul 14, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from sqlalchemy import func
31 31 from formencode import htmlfill
32 32 from pylons import request, session, tmpl_context as c, url, config
33 33 from pylons.controllers.util import abort, redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
38 38 HasPermissionAnyDecorator, NotAnonymous
39 39 from rhodecode.lib.base import BaseController, render
40 40 from rhodecode.lib.celerylib import tasks, run_task
41 41 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
42 42 set_rhodecode_config, repo_name_slug
43 from rhodecode.model.db import RhodeCodeUi, Repository, Group, \
44 RhodeCodeSettings
43 from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
44 RhodeCodeSetting
45 45 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
46 46 ApplicationUiSettingsForm
47 47 from rhodecode.model.scm import ScmModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.db import User
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.meta import Session
50 52
51 53 log = logging.getLogger(__name__)
52 54
53 55
54 56 class SettingsController(BaseController):
55 57 """REST Controller styled on the Atom Publishing Protocol"""
56 58 # To properly map this controller, ensure your config/routing.py
57 59 # file has a resource setup:
58 60 # map.resource('setting', 'settings', controller='admin/settings',
59 61 # path_prefix='/admin', name_prefix='admin_')
60 62
61 63 @LoginRequired()
62 64 def __before__(self):
63 65 c.admin_user = session.get('admin_user')
64 66 c.admin_username = session.get('admin_username')
65 67 super(SettingsController, self).__before__()
66 68
67 69 @HasPermissionAllDecorator('hg.admin')
68 70 def index(self, format='html'):
69 71 """GET /admin/settings: All items in the collection"""
70 72 # url('admin_settings')
71 73
72 defaults = RhodeCodeSettings.get_app_settings()
74 defaults = RhodeCodeSetting.get_app_settings()
73 75 defaults.update(self.get_hg_ui_settings())
74 76 return htmlfill.render(
75 77 render('admin/settings/settings.html'),
76 78 defaults=defaults,
77 79 encoding="UTF-8",
78 80 force_defaults=False
79 81 )
80 82
81 83 @HasPermissionAllDecorator('hg.admin')
82 84 def create(self):
83 85 """POST /admin/settings: Create a new item"""
84 86 # url('admin_settings')
85 87
86 88 @HasPermissionAllDecorator('hg.admin')
87 89 def new(self, format='html'):
88 90 """GET /admin/settings/new: Form to create a new item"""
89 91 # url('admin_new_setting')
90 92
91 93 @HasPermissionAllDecorator('hg.admin')
92 94 def update(self, setting_id):
93 95 """PUT /admin/settings/setting_id: Update an existing item"""
94 96 # Forms posted to this method should contain a hidden field:
95 97 # <input type="hidden" name="_method" value="PUT" />
96 98 # Or using helpers:
97 99 # h.form(url('admin_setting', setting_id=ID),
98 100 # method='put')
99 101 # url('admin_setting', setting_id=ID)
100 102 if setting_id == 'mapping':
101 103 rm_obsolete = request.POST.get('destroy', False)
102 log.debug('Rescanning directories with destroy=%s', rm_obsolete)
104 log.debug('Rescanning directories with destroy=%s' % rm_obsolete)
103 105 initial = ScmModel().repo_scan()
104 106 log.debug('invalidating all repositories')
105 107 for repo_name in initial.keys():
106 108 invalidate_cache('get_repo_cached_%s' % repo_name)
107 109
108 110 added, removed = repo2db_mapper(initial, rm_obsolete)
109 111
110 112 h.flash(_('Repositories successfully'
111 113 ' rescanned added: %s,removed: %s') % (added, removed),
112 114 category='success')
113 115
114 116 if setting_id == 'whoosh':
115 117 repo_location = self.get_hg_ui_settings()['paths_root_path']
116 118 full_index = request.POST.get('full_index', False)
117 119 run_task(tasks.whoosh_index, repo_location, full_index)
118 120
119 121 h.flash(_('Whoosh reindex task scheduled'), category='success')
120 122 if setting_id == 'global':
121 123
122 124 application_form = ApplicationSettingsForm()()
123 125 try:
124 126 form_result = application_form.to_python(dict(request.POST))
125 127
126 128 try:
127 hgsettings1 = RhodeCodeSettings.get_by_name('title')
129 hgsettings1 = RhodeCodeSetting.get_by_name('title')
128 130 hgsettings1.app_settings_value = \
129 131 form_result['rhodecode_title']
130 132
131 hgsettings2 = RhodeCodeSettings.get_by_name('realm')
133 hgsettings2 = RhodeCodeSetting.get_by_name('realm')
132 134 hgsettings2.app_settings_value = \
133 135 form_result['rhodecode_realm']
134 136
135 hgsettings3 = RhodeCodeSettings.get_by_name('ga_code')
137 hgsettings3 = RhodeCodeSetting.get_by_name('ga_code')
136 138 hgsettings3.app_settings_value = \
137 139 form_result['rhodecode_ga_code']
138 140
139 141 self.sa.add(hgsettings1)
140 142 self.sa.add(hgsettings2)
141 143 self.sa.add(hgsettings3)
142 144 self.sa.commit()
143 145 set_rhodecode_config(config)
144 146 h.flash(_('Updated application settings'),
145 147 category='success')
146 148
147 149 except Exception:
148 150 log.error(traceback.format_exc())
149 151 h.flash(_('error occurred during updating '
150 152 'application settings'),
151 153 category='error')
152 154
153 155 self.sa.rollback()
154 156
155 157 except formencode.Invalid, errors:
156 158 return htmlfill.render(
157 159 render('admin/settings/settings.html'),
158 160 defaults=errors.value,
159 161 errors=errors.error_dict or {},
160 162 prefix_error=False,
161 163 encoding="UTF-8")
162 164
163 165 if setting_id == 'mercurial':
164 166 application_form = ApplicationUiSettingsForm()()
165 167 try:
166 168 form_result = application_form.to_python(dict(request.POST))
167 169
168 170 try:
169 171
170 172 hgsettings1 = self.sa.query(RhodeCodeUi)\
171 173 .filter(RhodeCodeUi.ui_key == 'push_ssl').one()
172 174 hgsettings1.ui_value = form_result['web_push_ssl']
173 175
174 176 hgsettings2 = self.sa.query(RhodeCodeUi)\
175 177 .filter(RhodeCodeUi.ui_key == '/').one()
176 178 hgsettings2.ui_value = form_result['paths_root_path']
177 179
178 180 #HOOKS
179 181 hgsettings3 = self.sa.query(RhodeCodeUi)\
180 182 .filter(RhodeCodeUi.ui_key == 'changegroup.update').one()
181 183 hgsettings3.ui_active = \
182 184 bool(form_result['hooks_changegroup_update'])
183 185
184 186 hgsettings4 = self.sa.query(RhodeCodeUi)\
185 187 .filter(RhodeCodeUi.ui_key ==
186 188 'changegroup.repo_size').one()
187 189 hgsettings4.ui_active = \
188 190 bool(form_result['hooks_changegroup_repo_size'])
189 191
190 192 hgsettings5 = self.sa.query(RhodeCodeUi)\
191 193 .filter(RhodeCodeUi.ui_key ==
192 194 'pretxnchangegroup.push_logger').one()
193 195 hgsettings5.ui_active = \
194 196 bool(form_result['hooks_pretxnchangegroup'
195 197 '_push_logger'])
196 198
197 199 hgsettings6 = self.sa.query(RhodeCodeUi)\
198 200 .filter(RhodeCodeUi.ui_key ==
199 201 'preoutgoing.pull_logger').one()
200 202 hgsettings6.ui_active = \
201 203 bool(form_result['hooks_preoutgoing_pull_logger'])
202 204
203 205 self.sa.add(hgsettings1)
204 206 self.sa.add(hgsettings2)
205 207 self.sa.add(hgsettings3)
206 208 self.sa.add(hgsettings4)
207 209 self.sa.add(hgsettings5)
208 210 self.sa.add(hgsettings6)
209 211 self.sa.commit()
210 212
211 213 h.flash(_('Updated mercurial settings'),
212 214 category='success')
213 215
214 216 except:
215 217 log.error(traceback.format_exc())
216 218 h.flash(_('error occurred during updating '
217 219 'application settings'), category='error')
218 220
219 221 self.sa.rollback()
220 222
221 223 except formencode.Invalid, errors:
222 224 return htmlfill.render(
223 225 render('admin/settings/settings.html'),
224 226 defaults=errors.value,
225 227 errors=errors.error_dict or {},
226 228 prefix_error=False,
227 229 encoding="UTF-8")
228 230
229
230 231 if setting_id == 'hooks':
231 232 ui_key = request.POST.get('new_hook_ui_key')
232 233 ui_value = request.POST.get('new_hook_ui_value')
233 234 try:
234
235
235 236 if ui_value and ui_key:
236 237 RhodeCodeUi.create_or_update_hook(ui_key, ui_value)
237 238 h.flash(_('Added new hook'),
238 239 category='success')
239 240
240 241 # check for edits
241 242 update = False
242 243 _d = request.POST.dict_of_lists()
243 for k, v in zip(_d.get('hook_ui_key',[]), _d.get('hook_ui_value_new',[])):
244 for k, v in zip(_d.get('hook_ui_key', []),
245 _d.get('hook_ui_value_new', [])):
244 246 RhodeCodeUi.create_or_update_hook(k, v)
245 247 update = True
246 248
247 249 if update:
248 250 h.flash(_('Updated hooks'), category='success')
249
251 Session.commit()
250 252 except:
251 253 log.error(traceback.format_exc())
252 254 h.flash(_('error occurred during hook creation'),
253 255 category='error')
254 256
255 257 return redirect(url('admin_edit_setting', setting_id='hooks'))
256 258
259 if setting_id == 'email':
260 test_email = request.POST.get('test_email')
261 test_email_subj = 'RhodeCode TestEmail'
262 test_email_body = 'RhodeCode Email test'
263
264 test_email_html_body = EmailNotificationModel()\
265 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
266 body=test_email_body)
267
268 recipients = [test_email] if [test_email] else None
269
270 run_task(tasks.send_email, recipients, test_email_subj,
271 test_email_body, test_email_html_body)
272
273 h.flash(_('Email task created'), category='success')
257 274 return redirect(url('admin_settings'))
258 275
259 276 @HasPermissionAllDecorator('hg.admin')
260 277 def delete(self, setting_id):
261 278 """DELETE /admin/settings/setting_id: Delete an existing item"""
262 279 # Forms posted to this method should contain a hidden field:
263 280 # <input type="hidden" name="_method" value="DELETE" />
264 281 # Or using helpers:
265 282 # h.form(url('admin_setting', setting_id=ID),
266 283 # method='delete')
267 284 # url('admin_setting', setting_id=ID)
268 285 if setting_id == 'hooks':
269 286 hook_id = request.POST.get('hook_id')
270 287 RhodeCodeUi.delete(hook_id)
271
272
288
289
273 290 @HasPermissionAllDecorator('hg.admin')
274 291 def show(self, setting_id, format='html'):
275 292 """
276 293 GET /admin/settings/setting_id: Show a specific item"""
277 294 # url('admin_setting', setting_id=ID)
278 295
279 296 @HasPermissionAllDecorator('hg.admin')
280 297 def edit(self, setting_id, format='html'):
281 298 """
282 299 GET /admin/settings/setting_id/edit: Form to
283 300 edit an existing item"""
284 301 # url('admin_edit_setting', setting_id=ID)
285 302
286 303 c.hooks = RhodeCodeUi.get_builtin_hooks()
287 304 c.custom_hooks = RhodeCodeUi.get_custom_hooks()
288 305
289 306 return htmlfill.render(
290 307 render('admin/settings/hooks.html'),
291 308 defaults={},
292 309 encoding="UTF-8",
293 310 force_defaults=False
294 311 )
295 312
296 313 @NotAnonymous()
297 314 def my_account(self):
298 315 """
299 316 GET /_admin/my_account Displays info about my account
300 317 """
301 318 # url('admin_settings_my_account')
302 319
303 320 c.user = User.get(self.rhodecode_user.user_id)
304 321 all_repos = self.sa.query(Repository)\
305 322 .filter(Repository.user_id == c.user.user_id)\
306 323 .order_by(func.lower(Repository.repo_name)).all()
307 324
308 325 c.user_repos = ScmModel().get_repos(all_repos)
309 326
310 327 if c.user.username == 'default':
311 328 h.flash(_("You can't edit this user since it's"
312 329 " crucial for entire application"), category='warning')
313 330 return redirect(url('users'))
314 331
315 332 defaults = c.user.get_dict()
316 333 return htmlfill.render(
317 334 render('admin/users/user_edit_my_account.html'),
318 335 defaults=defaults,
319 336 encoding="UTF-8",
320 337 force_defaults=False
321 338 )
322 339
323 340 def my_account_update(self):
324 341 """PUT /_admin/my_account_update: Update an existing item"""
325 342 # Forms posted to this method should contain a hidden field:
326 343 # <input type="hidden" name="_method" value="PUT" />
327 344 # Or using helpers:
328 345 # h.form(url('admin_settings_my_account_update'),
329 346 # method='put')
330 347 # url('admin_settings_my_account_update', id=ID)
331 348 user_model = UserModel()
332 349 uid = self.rhodecode_user.user_id
333 350 _form = UserForm(edit=True,
334 351 old_data={'user_id': uid,
335 352 'email': self.rhodecode_user.email})()
336 353 form_result = {}
337 354 try:
338 355 form_result = _form.to_python(dict(request.POST))
339 356 user_model.update_my_account(uid, form_result)
340 357 h.flash(_('Your account was updated successfully'),
341 358 category='success')
342
359 Session.commit()
343 360 except formencode.Invalid, errors:
344 361 c.user = User.get(self.rhodecode_user.user_id)
345 362 all_repos = self.sa.query(Repository)\
346 363 .filter(Repository.user_id == c.user.user_id)\
347 364 .order_by(func.lower(Repository.repo_name))\
348 365 .all()
349 366 c.user_repos = ScmModel().get_repos(all_repos)
350 367
351 368 return htmlfill.render(
352 369 render('admin/users/user_edit_my_account.html'),
353 370 defaults=errors.value,
354 371 errors=errors.error_dict or {},
355 372 prefix_error=False,
356 373 encoding="UTF-8")
357 374 except Exception:
358 375 log.error(traceback.format_exc())
359 376 h.flash(_('error occurred during update of user %s') \
360 377 % form_result.get('username'), category='error')
361 378
362 379 return redirect(url('my_account'))
363 380
364 381 @NotAnonymous()
365 382 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
366 383 def create_repository(self):
367 384 """GET /_admin/create_repository: Form to create a new item"""
368 385
369 c.repo_groups = Group.groups_choices()
386 c.repo_groups = RepoGroup.groups_choices()
370 387 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
371 388
372 389 new_repo = request.GET.get('repo', '')
373 390 c.new_repo = repo_name_slug(new_repo)
374 391
375 392 return render('admin/repos/repo_add_create_repository.html')
376 393
377 394 def get_hg_ui_settings(self):
378 395 ret = self.sa.query(RhodeCodeUi).all()
379 396
380 397 if not ret:
381 398 raise Exception('Could not get application ui settings !')
382 399 settings = {}
383 400 for each in ret:
384 401 k = each.ui_key
385 402 v = each.ui_value
386 403 if k == '/':
387 404 k = 'root_path'
388 405
389 406 if k.find('.') != -1:
390 407 k = k.replace('.', '_')
391 408
392 409 if each.ui_section == 'hooks':
393 410 v = each.ui_active
394 411
395 412 settings[each.ui_section + '_' + k] = v
396 413
397 414 return settings
@@ -1,207 +1,210 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.users
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Users crud controller for pylons
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31 from pylons import request, session, tmpl_context as c, url, config
32 from pylons.controllers.util import abort, redirect
32 from pylons.controllers.util import redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.exceptions import DefaultUserException, \
36 36 UserOwnsReposException
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
39 39 from rhodecode.lib.base import BaseController, render
40 40
41 from rhodecode.model.db import User, RepoToPerm, UserToPerm, Permission
41 from rhodecode.model.db import User, Permission
42 42 from rhodecode.model.forms import UserForm
43 43 from rhodecode.model.user import UserModel
44 from rhodecode.model.meta import Session
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48
48 49 class UsersController(BaseController):
49 50 """REST Controller styled on the Atom Publishing Protocol"""
50 51 # To properly map this controller, ensure your config/routing.py
51 52 # file has a resource setup:
52 53 # map.resource('user', 'users')
53 54
54 55 @LoginRequired()
55 56 @HasPermissionAllDecorator('hg.admin')
56 57 def __before__(self):
57 58 c.admin_user = session.get('admin_user')
58 59 c.admin_username = session.get('admin_username')
59 60 super(UsersController, self).__before__()
60 61 c.available_permissions = config['available_permissions']
61 62
62 63 def index(self, format='html'):
63 64 """GET /users: All items in the collection"""
64 65 # url('users')
65 66
66 67 c.users_list = self.sa.query(User).all()
67 68 return render('admin/users/users.html')
68 69
69 70 def create(self):
70 71 """POST /users: Create a new item"""
71 72 # url('users')
72 73
73 74 user_model = UserModel()
74 login_form = UserForm()()
75 user_form = UserForm()()
75 76 try:
76 form_result = login_form.to_python(dict(request.POST))
77 form_result = user_form.to_python(dict(request.POST))
77 78 user_model.create(form_result)
78 79 h.flash(_('created user %s') % form_result['username'],
79 80 category='success')
81 Session.commit()
80 82 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
81 83 except formencode.Invalid, errors:
82 84 return htmlfill.render(
83 85 render('admin/users/user_add.html'),
84 86 defaults=errors.value,
85 87 errors=errors.error_dict or {},
86 88 prefix_error=False,
87 89 encoding="UTF-8")
88 90 except Exception:
89 91 log.error(traceback.format_exc())
90 92 h.flash(_('error occurred during creation of user %s') \
91 93 % request.POST.get('username'), category='error')
92 94 return redirect(url('users'))
93 95
94 96 def new(self, format='html'):
95 97 """GET /users/new: Form to create a new item"""
96 98 # url('new_user')
97 99 return render('admin/users/user_add.html')
98 100
99 101 def update(self, id):
100 102 """PUT /users/id: Update an existing item"""
101 103 # Forms posted to this method should contain a hidden field:
102 104 # <input type="hidden" name="_method" value="PUT" />
103 105 # Or using helpers:
104 106 # h.form(url('update_user', id=ID),
105 107 # method='put')
106 108 # url('user', id=ID)
107 109 user_model = UserModel()
108 110 c.user = user_model.get(id)
109 111
110 112 _form = UserForm(edit=True, old_data={'user_id': id,
111 113 'email': c.user.email})()
112 114 form_result = {}
113 115 try:
114 116 form_result = _form.to_python(dict(request.POST))
115 117 user_model.update(id, form_result)
116 118 h.flash(_('User updated successfully'), category='success')
117
119 Session.commit()
118 120 except formencode.Invalid, errors:
119 121 e = errors.error_dict or {}
120 122 perm = Permission.get_by_key('hg.create.repository')
121 e.update({'create_repo_perm': UserToPerm.has_perm(id, perm)})
123 e.update({'create_repo_perm': user_model.has_perm(id, perm)})
122 124 return htmlfill.render(
123 125 render('admin/users/user_edit.html'),
124 126 defaults=errors.value,
125 127 errors=e,
126 128 prefix_error=False,
127 129 encoding="UTF-8")
128 130 except Exception:
129 131 log.error(traceback.format_exc())
130 132 h.flash(_('error occurred during update of user %s') \
131 133 % form_result.get('username'), category='error')
132 134
133 135 return redirect(url('users'))
134 136
135 137 def delete(self, id):
136 138 """DELETE /users/id: Delete an existing item"""
137 139 # Forms posted to this method should contain a hidden field:
138 140 # <input type="hidden" name="_method" value="DELETE" />
139 141 # Or using helpers:
140 142 # h.form(url('delete_user', id=ID),
141 143 # method='delete')
142 144 # url('user', id=ID)
143 145 user_model = UserModel()
144 146 try:
145 147 user_model.delete(id)
146 148 h.flash(_('successfully deleted user'), category='success')
149 Session.commit()
147 150 except (UserOwnsReposException, DefaultUserException), e:
148 151 h.flash(str(e), category='warning')
149 152 except Exception:
150 153 h.flash(_('An error occurred during deletion of user'),
151 154 category='error')
152 155 return redirect(url('users'))
153 156
154 157 def show(self, id, format='html'):
155 158 """GET /users/id: Show a specific item"""
156 159 # url('user', id=ID)
157 160
158 161 def edit(self, id, format='html'):
159 162 """GET /users/id/edit: Form to edit an existing item"""
160 163 # url('edit_user', id=ID)
161 user_model = UserModel()
162 c.user = user_model.get(id)
164 c.user = User.get(id)
163 165 if not c.user:
164 166 return redirect(url('users'))
165 167 if c.user.username == 'default':
166 168 h.flash(_("You can't edit this user"), category='warning')
167 169 return redirect(url('users'))
168 170 c.user.permissions = {}
169 c.granted_permissions = user_model.fill_perms(c.user)\
171 c.granted_permissions = UserModel().fill_perms(c.user)\
170 172 .permissions['global']
171 173
172 174 defaults = c.user.get_dict()
173 175 perm = Permission.get_by_key('hg.create.repository')
174 defaults.update({'create_repo_perm': UserToPerm.has_perm(id, perm)})
176 defaults.update({'create_repo_perm': UserModel().has_perm(id, perm)})
175 177
176 178 return htmlfill.render(
177 179 render('admin/users/user_edit.html'),
178 180 defaults=defaults,
179 181 encoding="UTF-8",
180 182 force_defaults=False
181 183 )
182 184
183 185 def update_perm(self, id):
184 186 """PUT /users_perm/id: Update an existing item"""
185 187 # url('user_perm', id=ID, method='put')
186 188
187 189 grant_perm = request.POST.get('create_repo_perm', False)
190 user_model = UserModel()
188 191
189 192 if grant_perm:
190 193 perm = Permission.get_by_key('hg.create.none')
191 UserToPerm.revoke_perm(id, perm)
194 user_model.revoke_perm(id, perm)
192 195
193 196 perm = Permission.get_by_key('hg.create.repository')
194 UserToPerm.grant_perm(id, perm)
197 user_model.grant_perm(id, perm)
195 198 h.flash(_("Granted 'repository create' permission to user"),
196 199 category='success')
197
200 Session.commit()
198 201 else:
199 202 perm = Permission.get_by_key('hg.create.repository')
200 UserToPerm.revoke_perm(id, perm)
203 user_model.revoke_perm(id, perm)
201 204
202 205 perm = Permission.get_by_key('hg.create.none')
203 UserToPerm.grant_perm(id, perm)
206 user_model.grant_perm(id, perm)
204 207 h.flash(_("Revoked 'repository create' permission to user"),
205 208 category='success')
206
209 Session.commit()
207 210 return redirect(url('edit_user', id=id))
@@ -1,214 +1,226 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.users_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Users Groups crud controller for pylons
7 7
8 8 :created_on: Jan 25, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31 from pylons import request, session, tmpl_context as c, url, config
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.exceptions import UsersGroupsAssignedException
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h, safe_unicode
37 37 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 38 from rhodecode.lib.base import BaseController, render
39 39
40 from rhodecode.model.users_group import UsersGroupModel
41
40 42 from rhodecode.model.db import User, UsersGroup, Permission, UsersGroupToPerm
41 from rhodecode.model.forms import UserForm, UsersGroupForm
43 from rhodecode.model.forms import UsersGroupForm
44 from rhodecode.model.meta import Session
42 45
43 46 log = logging.getLogger(__name__)
44 47
45 48
46 49 class UsersGroupsController(BaseController):
47 50 """REST Controller styled on the Atom Publishing Protocol"""
48 51 # To properly map this controller, ensure your config/routing.py
49 52 # file has a resource setup:
50 53 # map.resource('users_group', 'users_groups')
51 54
52 55 @LoginRequired()
53 56 @HasPermissionAllDecorator('hg.admin')
54 57 def __before__(self):
55 58 c.admin_user = session.get('admin_user')
56 59 c.admin_username = session.get('admin_username')
57 60 super(UsersGroupsController, self).__before__()
58 61 c.available_permissions = config['available_permissions']
59 62
60 63 def index(self, format='html'):
61 64 """GET /users_groups: All items in the collection"""
62 65 # url('users_groups')
63 66 c.users_groups_list = self.sa.query(UsersGroup).all()
64 67 return render('admin/users_groups/users_groups.html')
65 68
66 69 def create(self):
67 70 """POST /users_groups: Create a new item"""
68 71 # url('users_groups')
69 72
70 73 users_group_form = UsersGroupForm()()
71 74 try:
72 75 form_result = users_group_form.to_python(dict(request.POST))
73 UsersGroup.create(form_result)
76 UsersGroupModel().create(name=form_result['users_group_name'],
77 active=form_result['users_group_active'])
74 78 h.flash(_('created users group %s') \
75 79 % form_result['users_group_name'], category='success')
76 80 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
81 Session.commit()
77 82 except formencode.Invalid, errors:
78 83 return htmlfill.render(
79 84 render('admin/users_groups/users_group_add.html'),
80 85 defaults=errors.value,
81 86 errors=errors.error_dict or {},
82 87 prefix_error=False,
83 88 encoding="UTF-8")
84 89 except Exception:
85 90 log.error(traceback.format_exc())
86 91 h.flash(_('error occurred during creation of users group %s') \
87 92 % request.POST.get('users_group_name'), category='error')
88 93
89 94 return redirect(url('users_groups'))
90 95
91 96 def new(self, format='html'):
92 97 """GET /users_groups/new: Form to create a new item"""
93 98 # url('new_users_group')
94 99 return render('admin/users_groups/users_group_add.html')
95 100
96 101 def update(self, id):
97 102 """PUT /users_groups/id: Update an existing item"""
98 103 # Forms posted to this method should contain a hidden field:
99 104 # <input type="hidden" name="_method" value="PUT" />
100 105 # Or using helpers:
101 106 # h.form(url('users_group', id=ID),
102 107 # method='put')
103 108 # url('users_group', id=ID)
104 109
105 110 c.users_group = UsersGroup.get(id)
106 c.group_members = [(x.user_id, x.user.username) for x in
107 c.users_group.members]
111 c.group_members_obj = [x.user for x in c.users_group.members]
112 c.group_members = [(x.user_id, x.username) for x in
113 c.group_members_obj]
108 114
109 115 c.available_members = [(x.user_id, x.username) for x in
110 116 self.sa.query(User).all()]
117
118 available_members = [safe_unicode(x[0]) for x in c.available_members]
119
111 120 users_group_form = UsersGroupForm(edit=True,
112 121 old_data=c.users_group.get_dict(),
113 available_members=[str(x[0]) for x
114 in c.available_members])()
122 available_members=available_members)()
115 123
116 124 try:
117 125 form_result = users_group_form.to_python(request.POST)
118 UsersGroup.update(id, form_result)
126 UsersGroupModel().update(c.users_group, form_result)
119 127 h.flash(_('updated users group %s') \
120 128 % form_result['users_group_name'],
121 129 category='success')
122 130 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
131 Session.commit()
123 132 except formencode.Invalid, errors:
124 133 e = errors.error_dict or {}
125 134
126 135 perm = Permission.get_by_key('hg.create.repository')
127 136 e.update({'create_repo_perm':
128 UsersGroupToPerm.has_perm(id, perm)})
137 UsersGroupModel().has_perm(id, perm)})
129 138
130 139 return htmlfill.render(
131 140 render('admin/users_groups/users_group_edit.html'),
132 141 defaults=errors.value,
133 142 errors=e,
134 143 prefix_error=False,
135 144 encoding="UTF-8")
136 145 except Exception:
137 146 log.error(traceback.format_exc())
138 147 h.flash(_('error occurred during update of users group %s') \
139 148 % request.POST.get('users_group_name'), category='error')
140 149
141 150 return redirect(url('users_groups'))
142 151
143 152 def delete(self, id):
144 153 """DELETE /users_groups/id: Delete an existing item"""
145 154 # Forms posted to this method should contain a hidden field:
146 155 # <input type="hidden" name="_method" value="DELETE" />
147 156 # Or using helpers:
148 157 # h.form(url('users_group', id=ID),
149 158 # method='delete')
150 159 # url('users_group', id=ID)
151 160
152 161 try:
153 UsersGroup.delete(id)
162 UsersGroupModel().delete(id)
154 163 h.flash(_('successfully deleted users group'), category='success')
164 Session.commit()
155 165 except UsersGroupsAssignedException, e:
156 166 h.flash(e, category='error')
157 167 except Exception:
158 168 h.flash(_('An error occurred during deletion of users group'),
159 169 category='error')
160 170 return redirect(url('users_groups'))
161 171
162 172 def show(self, id, format='html'):
163 173 """GET /users_groups/id: Show a specific item"""
164 174 # url('users_group', id=ID)
165 175
166 176 def edit(self, id, format='html'):
167 177 """GET /users_groups/id/edit: Form to edit an existing item"""
168 178 # url('edit_users_group', id=ID)
169 179
170 180 c.users_group = self.sa.query(UsersGroup).get(id)
171 181 if not c.users_group:
172 182 return redirect(url('users_groups'))
173 183
174 184 c.users_group.permissions = {}
175 c.group_members = [(x.user_id, x.user.username) for x in
176 c.users_group.members]
185 c.group_members_obj = [x.user for x in c.users_group.members]
186 c.group_members = [(x.user_id, x.username) for x in
187 c.group_members_obj]
177 188 c.available_members = [(x.user_id, x.username) for x in
178 189 self.sa.query(User).all()]
179 190 defaults = c.users_group.get_dict()
180 191 perm = Permission.get_by_key('hg.create.repository')
181 192 defaults.update({'create_repo_perm':
182 UsersGroupToPerm.has_perm(id, perm)})
193 UsersGroupModel().has_perm(c.users_group, perm)})
183 194 return htmlfill.render(
184 195 render('admin/users_groups/users_group_edit.html'),
185 196 defaults=defaults,
186 197 encoding="UTF-8",
187 198 force_defaults=False
188 199 )
189 200
190 201 def update_perm(self, id):
191 202 """PUT /users_perm/id: Update an existing item"""
192 203 # url('users_group_perm', id=ID, method='put')
193 204
194 205 grant_perm = request.POST.get('create_repo_perm', False)
195 206
196 207 if grant_perm:
197 208 perm = Permission.get_by_key('hg.create.none')
198 UsersGroupToPerm.revoke_perm(id, perm)
209 UsersGroupModel().revoke_perm(id, perm)
199 210
200 211 perm = Permission.get_by_key('hg.create.repository')
201 UsersGroupToPerm.grant_perm(id, perm)
212 UsersGroupModel().grant_perm(id, perm)
202 213 h.flash(_("Granted 'repository create' permission to user"),
203 214 category='success')
204 215
216 Session.commit()
205 217 else:
206 218 perm = Permission.get_by_key('hg.create.repository')
207 UsersGroupToPerm.revoke_perm(id, perm)
219 UsersGroupModel().revoke_perm(id, perm)
208 220
209 221 perm = Permission.get_by_key('hg.create.none')
210 UsersGroupToPerm.grant_perm(id, perm)
222 UsersGroupModel().grant_perm(id, perm)
211 223 h.flash(_("Revoked 'repository create' permission to user"),
212 224 category='success')
213
225 Session.commit()
214 226 return redirect(url('edit_users_group', id=id))
@@ -1,256 +1,262 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 JSON RPC controller
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 #
17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 #
22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import inspect
29 29 import logging
30 30 import types
31 31 import urllib
32 32 import traceback
33 33
34 34 from rhodecode.lib.compat import izip_longest, json
35 35
36 36 from paste.response import replace_header
37 37
38 38 from pylons.controllers import WSGIController
39 39
40 40
41 41 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
42 42 HTTPBadRequest, HTTPError
43 43
44 44 from rhodecode.model.db import User
45 45 from rhodecode.lib.auth import AuthUser
46 46
47 47 log = logging.getLogger('JSONRPC')
48 48
49 49
50 50 class JSONRPCError(BaseException):
51 51
52 52 def __init__(self, message):
53 53 self.message = message
54 54 super(JSONRPCError, self).__init__()
55 55
56 56 def __str__(self):
57 57 return str(self.message)
58 58
59 59
60 60 def jsonrpc_error(message, code=None):
61 61 """
62 62 Generate a Response object with a JSON-RPC error body
63 63 """
64 64 from pylons.controllers.util import Response
65 resp = Response(body=json.dumps(dict(result=None, error=message)),
65 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
66 66 status=code,
67 67 content_type='application/json')
68 68 return resp
69 69
70 70
71 71 class JSONRPCController(WSGIController):
72 72 """
73 73 A WSGI-speaking JSON-RPC controller class
74 74
75 75 See the specification:
76 76 <http://json-rpc.org/wiki/specification>`.
77 77
78 78 Valid controller return values should be json-serializable objects.
79 79
80 80 Sub-classes should catch their exceptions and raise JSONRPCError
81 81 if they want to pass meaningful errors to the client.
82 82
83 83 """
84 84
85 85 def _get_method_args(self):
86 86 """
87 87 Return `self._rpc_args` to dispatched controller method
88 88 chosen by __call__
89 89 """
90 90 return self._rpc_args
91 91
92 92 def __call__(self, environ, start_response):
93 93 """
94 94 Parse the request body as JSON, look up the method on the
95 95 controller and if it exists, dispatch to it.
96 96 """
97 97 if 'CONTENT_LENGTH' not in environ:
98 98 log.debug("No Content-Length")
99 99 return jsonrpc_error(message="No Content-Length in request")
100 100 else:
101 101 length = environ['CONTENT_LENGTH'] or 0
102 102 length = int(environ['CONTENT_LENGTH'])
103 log.debug('Content-Length: %s', length)
103 log.debug('Content-Length: %s' % length)
104 104
105 105 if length == 0:
106 106 log.debug("Content-Length is 0")
107 107 return jsonrpc_error(message="Content-Length is 0")
108 108
109 109 raw_body = environ['wsgi.input'].read(length)
110 110
111 111 try:
112 112 json_body = json.loads(urllib.unquote_plus(raw_body))
113 113 except ValueError, e:
114 114 # catch JSON errors Here
115 115 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
116 116 % (e, urllib.unquote_plus(raw_body)))
117 117
118 118 # check AUTH based on API KEY
119 119 try:
120 120 self._req_api_key = json_body['api_key']
121 self._req_id = json_body['id']
121 122 self._req_method = json_body['method']
122 123 self._request_params = json_body['args']
123 log.debug('method: %s, params: %s',
124 self._req_method,
125 self._request_params)
124 log.debug(
125 'method: %s, params: %s' % (self._req_method,
126 self._request_params)
127 )
126 128 except KeyError, e:
127 129 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
128 130
129 131 # check if we can find this session using api_key
130 132 try:
131 133 u = User.get_by_api_key(self._req_api_key)
132 134 if u is None:
133 135 return jsonrpc_error(message='Invalid API KEY')
134 136 auth_u = AuthUser(u.user_id, self._req_api_key)
135 137 except Exception, e:
136 138 return jsonrpc_error(message='Invalid API KEY')
137 139
138 140 self._error = None
139 141 try:
140 142 self._func = self._find_method()
141 143 except AttributeError, e:
142 144 return jsonrpc_error(message=str(e))
143 145
144 146 # now that we have a method, add self._req_params to
145 147 # self.kargs and dispatch control to WGIController
146 148 argspec = inspect.getargspec(self._func)
147 149 arglist = argspec[0][1:]
148 150 defaults = map(type, argspec[3] or [])
149 151 default_empty = types.NotImplementedType
150 152
151 153 # kw arguments required by this method
152 154 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
153 155 fillvalue=default_empty))
154 156
155 157 # this is little trick to inject logged in user for
156 158 # perms decorators to work they expect the controller class to have
157 159 # rhodecode_user attribute set
158 160 self.rhodecode_user = auth_u
159 161
160 162 # This attribute will need to be first param of a method that uses
161 163 # api_key, which is translated to instance of user at that name
162 164 USER_SESSION_ATTR = 'apiuser'
163 165
164 166 if USER_SESSION_ATTR not in arglist:
165 167 return jsonrpc_error(message='This method [%s] does not support '
166 168 'authentication (missing %s param)' %
167 169 (self._func.__name__, USER_SESSION_ATTR))
168 170
169 171 # get our arglist and check if we provided them as args
170 172 for arg, default in func_kwargs.iteritems():
171 173 if arg == USER_SESSION_ATTR:
172 174 # USER_SESSION_ATTR is something translated from api key and
173 175 # this is checked before so we don't need validate it
174 176 continue
175 177
176 178 # skip the required param check if it's default value is
177 179 # NotImplementedType (default_empty)
178 180 if (default == default_empty and arg not in self._request_params):
179 181 return jsonrpc_error(
180 182 message=(
181 183 'Missing non optional `%s` arg in JSON DATA' % arg
182 184 )
183 185 )
184 186
185 187 self._rpc_args = {USER_SESSION_ATTR: u}
186 188 self._rpc_args.update(self._request_params)
187 189
188 190 self._rpc_args['action'] = self._req_method
189 191 self._rpc_args['environ'] = environ
190 192 self._rpc_args['start_response'] = start_response
191 193
192 194 status = []
193 195 headers = []
194 196 exc_info = []
195 197
196 198 def change_content(new_status, new_headers, new_exc_info=None):
197 199 status.append(new_status)
198 200 headers.extend(new_headers)
199 201 exc_info.append(new_exc_info)
200 202
201 203 output = WSGIController.__call__(self, environ, change_content)
202 204 output = list(output)
203 205 headers.append(('Content-Length', str(len(output[0]))))
204 206 replace_header(headers, 'Content-Type', 'application/json')
205 207 start_response(status[0], headers, exc_info[0])
206 208
207 209 return output
208 210
209 211 def _dispatch_call(self):
210 212 """
211 213 Implement dispatch interface specified by WSGIController
212 214 """
213 215 try:
214 216 raw_response = self._inspect_call(self._func)
215 217 if isinstance(raw_response, HTTPError):
216 218 self._error = str(raw_response)
217 219 except JSONRPCError, e:
218 220 self._error = str(e)
219 221 except Exception, e:
220 222 log.error('Encountered unhandled exception: %s' \
221 223 % traceback.format_exc())
222 224 json_exc = JSONRPCError('Internal server error')
223 225 self._error = str(json_exc)
224 226
225 227 if self._error is not None:
226 228 raw_response = None
227 229
228 response = dict(result=raw_response,
230 response = dict(id=self._req_id, result=raw_response,
229 231 error=self._error)
230 232
231 233 try:
232 234 return json.dumps(response)
233 235 except TypeError, e:
234 log.debug('Error encoding response: %s', e)
235 return json.dumps(dict(result=None,
236 error="Error encoding response"))
236 log.debug('Error encoding response: %s' % e)
237 return json.dumps(
238 dict(
239 self._req_id,
240 result=None,
241 error="Error encoding response"
242 )
243 )
237 244
238 245 def _find_method(self):
239 246 """
240 247 Return method named by `self._req_method` in controller if able
241 248 """
242 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
249 log.debug('Trying to find JSON-RPC method: %s' % self._req_method)
243 250 if self._req_method.startswith('_'):
244 251 raise AttributeError("Method not allowed")
245 252
246 253 try:
247 254 func = getattr(self, self._req_method, None)
248 255 except UnicodeEncodeError:
249 256 raise AttributeError("Problem decoding unicode in requested "
250 257 "method name.")
251 258
252 259 if isinstance(func, types.MethodType):
253 260 return func
254 261 else:
255 262 raise AttributeError("No such method: %s" % self._req_method)
256
@@ -1,422 +1,660 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 API controller for RhodeCode
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import traceback
29 29 import logging
30 30
31 31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
32 32 from rhodecode.lib.auth import HasPermissionAllDecorator, \
33 HasPermissionAnyDecorator
33 HasPermissionAnyDecorator, PasswordGenerator
34
35 from rhodecode.model.meta import Session
34 36 from rhodecode.model.scm import ScmModel
35
36 from rhodecode.model.db import User, UsersGroup, Group, Repository
37 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
37 38 from rhodecode.model.repo import RepoModel
38 39 from rhodecode.model.user import UserModel
39 from rhodecode.model.repo_permission import RepositoryPermissionModel
40 40 from rhodecode.model.users_group import UsersGroupModel
41 from rhodecode.model import users_group
42 41 from rhodecode.model.repos_group import ReposGroupModel
43 from sqlalchemy.orm.exc import NoResultFound
44 42
45 43
46 44 log = logging.getLogger(__name__)
47 45
48 46
49 47 class ApiController(JSONRPCController):
50 48 """
51 49 API Controller
52 50
53 51
54 52 Each method needs to have USER as argument this is then based on given
55 53 API_KEY propagated as instance of user object
56 54
57 55 Preferably this should be first argument also
58 56
59 57
60 58 Each function should also **raise** JSONRPCError for any
61 59 errors that happens
62 60
63 61 """
64 62
65 63 @HasPermissionAllDecorator('hg.admin')
66 def pull(self, apiuser, repo):
64 def pull(self, apiuser, repo_name):
67 65 """
68 66 Dispatch pull action on given repo
69 67
70 68
71 69 :param user:
72 :param repo:
70 :param repo_name:
73 71 """
74 72
75 if Repository.is_valid(repo) is False:
76 raise JSONRPCError('Unknown repo "%s"' % repo)
73 if Repository.is_valid(repo_name) is False:
74 raise JSONRPCError('Unknown repo "%s"' % repo_name)
77 75
78 76 try:
79 ScmModel().pull_changes(repo, self.rhodecode_user.username)
80 return 'Pulled from %s' % repo
77 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
78 return 'Pulled from %s' % repo_name
81 79 except Exception:
82 raise JSONRPCError('Unable to pull changes from "%s"' % repo)
80 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
83 81
84 82 @HasPermissionAllDecorator('hg.admin')
85 def get_user(self, apiuser, username):
83 def get_user(self, apiuser, userid):
86 84 """"
87 85 Get a user by username
88 86
89 87 :param apiuser:
90 88 :param username:
91 89 """
92 90
93 user = User.get_by_username(username)
94 if not user:
95 return None
91 user = UserModel().get_user(userid)
92 if user is None:
93 return user
96 94
97 95 return dict(
98 96 id=user.user_id,
99 97 username=user.username,
100 98 firstname=user.name,
101 99 lastname=user.lastname,
102 100 email=user.email,
103 101 active=user.active,
104 102 admin=user.admin,
105 ldap=user.ldap_dn
103 ldap_dn=user.ldap_dn
106 104 )
107 105
108 106 @HasPermissionAllDecorator('hg.admin')
109 107 def get_users(self, apiuser):
110 108 """"
111 109 Get all users
112 110
113 111 :param apiuser:
114 112 """
115 113
116 114 result = []
117 115 for user in User.getAll():
118 116 result.append(
119 117 dict(
120 118 id=user.user_id,
121 119 username=user.username,
122 120 firstname=user.name,
123 121 lastname=user.lastname,
124 122 email=user.email,
125 123 active=user.active,
126 124 admin=user.admin,
127 ldap=user.ldap_dn
125 ldap_dn=user.ldap_dn
128 126 )
129 127 )
130 128 return result
131 129
132 130 @HasPermissionAllDecorator('hg.admin')
133 def create_user(self, apiuser, username, password, firstname,
134 lastname, email, active=True, admin=False, ldap_dn=None):
131 def create_user(self, apiuser, username, email, password, firstname=None,
132 lastname=None, active=True, admin=False, ldap_dn=None):
135 133 """
136 134 Create new user
137 135
138 136 :param apiuser:
139 137 :param username:
140 138 :param password:
139 :param email:
141 140 :param name:
142 141 :param lastname:
143 :param email:
144 142 :param active:
145 143 :param admin:
146 144 :param ldap_dn:
147 145 """
148
149 146 if User.get_by_username(username):
150 147 raise JSONRPCError("user %s already exist" % username)
151 148
149 if User.get_by_email(email, case_insensitive=True):
150 raise JSONRPCError("email %s already exist" % email)
151
152 if ldap_dn:
153 # generate temporary password if ldap_dn
154 password = PasswordGenerator().gen_password(length=8)
155
152 156 try:
153 form_data = dict(username=username,
154 password=password,
155 active=active,
156 admin=admin,
157 name=firstname,
158 lastname=lastname,
159 email=email,
160 ldap_dn=ldap_dn)
161 UserModel().create_ldap(username, password, ldap_dn, form_data)
162 return dict(msg='created new user %s' % username)
157 usr = UserModel().create_or_update(
158 username, password, email, firstname,
159 lastname, active, admin, ldap_dn
160 )
161 Session.commit()
162 return dict(
163 id=usr.user_id,
164 msg='created new user %s' % username
165 )
163 166 except Exception:
164 167 log.error(traceback.format_exc())
165 168 raise JSONRPCError('failed to create user %s' % username)
166 169
167 170 @HasPermissionAllDecorator('hg.admin')
171 def update_user(self, apiuser, userid, username, password, email,
172 firstname, lastname, active, admin, ldap_dn):
173 """
174 Updates given user
175
176 :param apiuser:
177 :param username:
178 :param password:
179 :param email:
180 :param name:
181 :param lastname:
182 :param active:
183 :param admin:
184 :param ldap_dn:
185 """
186 if not UserModel().get_user(userid):
187 raise JSONRPCError("user %s does not exist" % username)
188
189 try:
190 usr = UserModel().create_or_update(
191 username, password, email, firstname,
192 lastname, active, admin, ldap_dn
193 )
194 Session.commit()
195 return dict(
196 id=usr.user_id,
197 msg='updated user %s' % username
198 )
199 except Exception:
200 log.error(traceback.format_exc())
201 raise JSONRPCError('failed to update user %s' % username)
202
203 @HasPermissionAllDecorator('hg.admin')
168 204 def get_users_group(self, apiuser, group_name):
169 205 """"
170 206 Get users group by name
171 207
172 208 :param apiuser:
173 209 :param group_name:
174 210 """
175 211
176 212 users_group = UsersGroup.get_by_group_name(group_name)
177 213 if not users_group:
178 214 return None
179 215
180 216 members = []
181 217 for user in users_group.members:
182 218 user = user.user
183 219 members.append(dict(id=user.user_id,
184 220 username=user.username,
185 221 firstname=user.name,
186 222 lastname=user.lastname,
187 223 email=user.email,
188 224 active=user.active,
189 225 admin=user.admin,
190 226 ldap=user.ldap_dn))
191 227
192 228 return dict(id=users_group.users_group_id,
193 name=users_group.users_group_name,
229 group_name=users_group.users_group_name,
194 230 active=users_group.users_group_active,
195 231 members=members)
196 232
197 233 @HasPermissionAllDecorator('hg.admin')
198 234 def get_users_groups(self, apiuser):
199 235 """"
200 236 Get all users groups
201 237
202 238 :param apiuser:
203 239 """
204 240
205 241 result = []
206 242 for users_group in UsersGroup.getAll():
207 243 members = []
208 244 for user in users_group.members:
209 245 user = user.user
210 246 members.append(dict(id=user.user_id,
211 247 username=user.username,
212 248 firstname=user.name,
213 249 lastname=user.lastname,
214 250 email=user.email,
215 251 active=user.active,
216 252 admin=user.admin,
217 253 ldap=user.ldap_dn))
218 254
219 255 result.append(dict(id=users_group.users_group_id,
220 name=users_group.users_group_name,
256 group_name=users_group.users_group_name,
221 257 active=users_group.users_group_active,
222 258 members=members))
223 259 return result
224 260
225 261 @HasPermissionAllDecorator('hg.admin')
226 def create_users_group(self, apiuser, name, active=True):
262 def create_users_group(self, apiuser, group_name, active=True):
227 263 """
228 264 Creates an new usergroup
229 265
230 :param name:
266 :param group_name:
231 267 :param active:
232 268 """
233 269
234 if self.get_users_group(apiuser, name):
235 raise JSONRPCError("users group %s already exist" % name)
270 if self.get_users_group(apiuser, group_name):
271 raise JSONRPCError("users group %s already exist" % group_name)
236 272
237 273 try:
238 form_data = dict(users_group_name=name,
239 users_group_active=active)
240 ug = UsersGroup.create(form_data)
274 ug = UsersGroupModel().create(name=group_name, active=active)
275 Session.commit()
241 276 return dict(id=ug.users_group_id,
242 msg='created new users group %s' % name)
277 msg='created new users group %s' % group_name)
243 278 except Exception:
244 279 log.error(traceback.format_exc())
245 raise JSONRPCError('failed to create group %s' % name)
280 raise JSONRPCError('failed to create group %s' % group_name)
246 281
247 282 @HasPermissionAllDecorator('hg.admin')
248 def add_user_to_users_group(self, apiuser, group_name, user_name):
283 def add_user_to_users_group(self, apiuser, group_name, username):
249 284 """"
250 285 Add a user to a group
251 286
252 :param apiuser
253 :param group_name
254 :param user_name
287 :param apiuser:
288 :param group_name:
289 :param username:
255 290 """
256 291
257 292 try:
258 293 users_group = UsersGroup.get_by_group_name(group_name)
259 294 if not users_group:
260 295 raise JSONRPCError('unknown users group %s' % group_name)
261 296
262 try:
263 user = User.get_by_username(user_name)
264 except NoResultFound:
265 raise JSONRPCError('unknown user %s' % user_name)
297 user = User.get_by_username(username)
298 if user is None:
299 raise JSONRPCError('unknown user %s' % username)
266 300
267 301 ugm = UsersGroupModel().add_user_to_group(users_group, user)
302 success = True if ugm != True else False
303 msg = 'added member %s to users group %s' % (username, group_name)
304 msg = msg if success else 'User is already in that group'
305 Session.commit()
268 306
269 return dict(id=ugm.users_group_member_id,
270 msg='created new users group member')
307 return dict(
308 id=ugm.users_group_member_id if ugm != True else None,
309 success=success,
310 msg=msg
311 )
271 312 except Exception:
272 313 log.error(traceback.format_exc())
273 raise JSONRPCError('failed to create users group member')
314 raise JSONRPCError('failed to add users group member')
315
316 @HasPermissionAllDecorator('hg.admin')
317 def remove_user_from_users_group(self, apiuser, group_name, username):
318 """
319 Remove user from a group
320
321 :param apiuser
322 :param group_name
323 :param username
324 """
325
326 try:
327 users_group = UsersGroup.get_by_group_name(group_name)
328 if not users_group:
329 raise JSONRPCError('unknown users group %s' % group_name)
330
331 user = User.get_by_username(username)
332 if user is None:
333 raise JSONRPCError('unknown user %s' % username)
334
335 success = UsersGroupModel().remove_user_from_group(users_group, user)
336 msg = 'removed member %s from users group %s' % (username, group_name)
337 msg = msg if success else "User wasn't in group"
338 Session.commit()
339 return dict(success=success, msg=msg)
340 except Exception:
341 log.error(traceback.format_exc())
342 raise JSONRPCError('failed to remove user from group')
274 343
275 344 @HasPermissionAnyDecorator('hg.admin')
276 def get_repo(self, apiuser, name):
345 def get_repo(self, apiuser, repoid):
277 346 """"
278 347 Get repository by name
279 348
280 :param apiuser
281 :param repo_name
349 :param apiuser:
350 :param repo_name:
282 351 """
283 352
284 try:
285 repo = Repository.get_by_repo_name(name)
286 except NoResultFound:
287 return None
353 repo = RepoModel().get_repo(repoid)
354 if repo is None:
355 raise JSONRPCError('unknown repository %s' % repo)
288 356
289 357 members = []
290 358 for user in repo.repo_to_perm:
291 359 perm = user.permission.permission_name
292 360 user = user.user
293 361 members.append(
294 362 dict(
295 363 type_="user",
296 364 id=user.user_id,
297 365 username=user.username,
298 366 firstname=user.name,
299 367 lastname=user.lastname,
300 368 email=user.email,
301 369 active=user.active,
302 370 admin=user.admin,
303 371 ldap=user.ldap_dn,
304 372 permission=perm
305 373 )
306 374 )
307 375 for users_group in repo.users_group_to_perm:
308 376 perm = users_group.permission.permission_name
309 377 users_group = users_group.users_group
310 378 members.append(
311 379 dict(
312 380 type_="users_group",
313 381 id=users_group.users_group_id,
314 382 name=users_group.users_group_name,
315 383 active=users_group.users_group_active,
316 384 permission=perm
317 385 )
318 386 )
319 387
320 388 return dict(
321 389 id=repo.repo_id,
322 name=repo.repo_name,
390 repo_name=repo.repo_name,
323 391 type=repo.repo_type,
324 392 description=repo.description,
325 393 members=members
326 394 )
327 395
328 396 @HasPermissionAnyDecorator('hg.admin')
329 397 def get_repos(self, apiuser):
330 398 """"
331 399 Get all repositories
332 400
333 :param apiuser
401 :param apiuser:
334 402 """
335 403
336 404 result = []
337 405 for repository in Repository.getAll():
338 406 result.append(
339 407 dict(
340 408 id=repository.repo_id,
341 name=repository.repo_name,
409 repo_name=repository.repo_name,
342 410 type=repository.repo_type,
343 411 description=repository.description
344 412 )
345 413 )
346 414 return result
347 415
348 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
349 def create_repo(self, apiuser, name, owner_name, description='',
350 repo_type='hg', private=False):
416 @HasPermissionAnyDecorator('hg.admin')
417 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
418 ret_type='all'):
351 419 """
352 Create a repository
420 returns a list of nodes and it's children
421 for a given path at given revision. It's possible to specify ret_type
422 to show only files or dirs
353 423
354 :param apiuser
355 :param name
356 :param description
357 :param type
358 :param private
359 :param owner_name
424 :param apiuser:
425 :param repo_name: name of repository
426 :param revision: revision for which listing should be done
427 :param root_path: path from which start displaying
428 :param ret_type: return type 'all|files|dirs' nodes
429 """
430 try:
431 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
432 flat=False)
433 _map = {
434 'all': _d + _f,
435 'files': _f,
436 'dirs': _d,
437 }
438 return _map[ret_type]
439 except KeyError:
440 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
441 except Exception, e:
442 raise JSONRPCError(e)
443
444 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
445 def create_repo(self, apiuser, repo_name, owner_name, description='',
446 repo_type='hg', private=False, clone_uri=None):
447 """
448 Create repository, if clone_url is given it makes a remote clone
449
450 :param apiuser:
451 :param repo_name:
452 :param owner_name:
453 :param description:
454 :param repo_type:
455 :param private:
456 :param clone_uri:
360 457 """
361 458
362 459 try:
363 try:
364 owner = User.get_by_username(owner_name)
365 except NoResultFound:
366 raise JSONRPCError('unknown user %s' % owner)
460 owner = User.get_by_username(owner_name)
461 if owner is None:
462 raise JSONRPCError('unknown user %s' % owner_name)
367 463
368 if self.get_repo(apiuser, name):
369 raise JSONRPCError("repo %s already exist" % name)
464 if Repository.get_by_repo_name(repo_name):
465 raise JSONRPCError("repo %s already exist" % repo_name)
370 466
371 groups = name.split('/')
467 groups = repo_name.split('/')
372 468 real_name = groups[-1]
373 469 groups = groups[:-1]
374 470 parent_id = None
375 471 for g in groups:
376 group = Group.get_by_group_name(g)
472 group = RepoGroup.get_by_group_name(g)
377 473 if not group:
378 group = ReposGroupModel().create(dict(group_name=g,
379 group_description='',
380 group_parent_id=parent_id))
474 group = ReposGroupModel().create(g, '', parent_id)
381 475 parent_id = group.group_id
382 476
383 RepoModel().create(dict(repo_name=real_name,
384 repo_name_full=name,
385 description=description,
386 private=private,
387 repo_type=repo_type,
388 repo_group=parent_id,
389 clone_uri=None), owner)
477 repo = RepoModel().create(
478 dict(
479 repo_name=real_name,
480 repo_name_full=repo_name,
481 description=description,
482 private=private,
483 repo_type=repo_type,
484 repo_group=parent_id,
485 clone_uri=clone_uri
486 ),
487 owner
488 )
489 Session.commit()
490
491 return dict(
492 id=repo.repo_id,
493 msg="Created new repository %s" % repo.repo_name
494 )
495
496 except Exception:
497 log.error(traceback.format_exc())
498 raise JSONRPCError('failed to create repository %s' % repo_name)
499
500 @HasPermissionAnyDecorator('hg.admin')
501 def delete_repo(self, apiuser, repo_name):
502 """
503 Deletes a given repository
504
505 :param repo_name:
506 """
507 if not Repository.get_by_repo_name(repo_name):
508 raise JSONRPCError("repo %s does not exist" % repo_name)
509 try:
510 RepoModel().delete(repo_name)
511 Session.commit()
512 return dict(
513 msg='Deleted repository %s' % repo_name
514 )
390 515 except Exception:
391 516 log.error(traceback.format_exc())
392 raise JSONRPCError('failed to create repository %s' % name)
517 raise JSONRPCError('failed to delete repository %s' % repo_name)
393 518
394 519 @HasPermissionAnyDecorator('hg.admin')
395 def add_user_to_repo(self, apiuser, repo_name, user_name, perm):
520 def grant_user_permission(self, apiuser, repo_name, username, perm):
521 """
522 Grant permission for user on given repository, or update existing one
523 if found
524
525 :param repo_name:
526 :param username:
527 :param perm:
396 528 """
397 Add permission for a user to a repository
529
530 try:
531 repo = Repository.get_by_repo_name(repo_name)
532 if repo is None:
533 raise JSONRPCError('unknown repository %s' % repo)
534
535 user = User.get_by_username(username)
536 if user is None:
537 raise JSONRPCError('unknown user %s' % username)
538
539 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
398 540
399 :param apiuser
400 :param repo_name
401 :param user_name
402 :param perm
541 Session.commit()
542 return dict(
543 msg='Granted perm: %s for user: %s in repo: %s' % (
544 perm, username, repo_name
545 )
546 )
547 except Exception:
548 log.error(traceback.format_exc())
549 raise JSONRPCError(
550 'failed to edit permission %(repo)s for %(user)s' % dict(
551 user=username, repo=repo_name
552 )
553 )
554
555 @HasPermissionAnyDecorator('hg.admin')
556 def revoke_user_permission(self, apiuser, repo_name, username):
557 """
558 Revoke permission for user on given repository
559
560 :param repo_name:
561 :param username:
403 562 """
404 563
405 564 try:
406 try:
407 repo = Repository.get_by_repo_name(repo_name)
408 except NoResultFound:
565 repo = Repository.get_by_repo_name(repo_name)
566 if repo is None:
567 raise JSONRPCError('unknown repository %s' % repo)
568
569 user = User.get_by_username(username)
570 if user is None:
571 raise JSONRPCError('unknown user %s' % username)
572
573 RepoModel().revoke_user_permission(repo=repo_name, user=username)
574
575 Session.commit()
576 return dict(
577 msg='Revoked perm for user: %s in repo: %s' % (
578 username, repo_name
579 )
580 )
581 except Exception:
582 log.error(traceback.format_exc())
583 raise JSONRPCError(
584 'failed to edit permission %(repo)s for %(user)s' % dict(
585 user=username, repo=repo_name
586 )
587 )
588
589 @HasPermissionAnyDecorator('hg.admin')
590 def grant_users_group_permission(self, apiuser, repo_name, group_name, perm):
591 """
592 Grant permission for users group on given repository, or update
593 existing one if found
594
595 :param repo_name:
596 :param group_name:
597 :param perm:
598 """
599
600 try:
601 repo = Repository.get_by_repo_name(repo_name)
602 if repo is None:
409 603 raise JSONRPCError('unknown repository %s' % repo)
410 604
411 try:
412 user = User.get_by_username(user_name)
413 except NoResultFound:
414 raise JSONRPCError('unknown user %s' % user)
605 user_group = UsersGroup.get_by_group_name(group_name)
606 if user_group is None:
607 raise JSONRPCError('unknown users group %s' % user_group)
415 608
416 RepositoryPermissionModel()\
417 .update_or_delete_user_permission(repo, user, perm)
609 RepoModel().grant_users_group_permission(repo=repo_name,
610 group_name=group_name,
611 perm=perm)
612
613 Session.commit()
614 return dict(
615 msg='Granted perm: %s for group: %s in repo: %s' % (
616 perm, group_name, repo_name
617 )
618 )
418 619 except Exception:
419 620 log.error(traceback.format_exc())
420 raise JSONRPCError('failed to edit permission %(repo)s for %(user)s'
421 % dict(user=user_name, repo=repo_name))
621 raise JSONRPCError(
622 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
623 usersgr=group_name, repo=repo_name
624 )
625 )
626
627 @HasPermissionAnyDecorator('hg.admin')
628 def revoke_users_group_permission(self, apiuser, repo_name, group_name):
629 """
630 Revoke permission for users group on given repository
631
632 :param repo_name:
633 :param group_name:
634 """
635
636 try:
637 repo = Repository.get_by_repo_name(repo_name)
638 if repo is None:
639 raise JSONRPCError('unknown repository %s' % repo)
422 640
641 user_group = UsersGroup.get_by_group_name(group_name)
642 if user_group is None:
643 raise JSONRPCError('unknown users group %s' % user_group)
644
645 RepoModel().revoke_users_group_permission(repo=repo_name,
646 group_name=group_name)
647
648 Session.commit()
649 return dict(
650 msg='Revoked perm for group: %s in repo: %s' % (
651 group_name, repo_name
652 )
653 )
654 except Exception:
655 log.error(traceback.format_exc())
656 raise JSONRPCError(
657 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
658 usersgr=group_name, repo=repo_name
659 )
660 )
@@ -1,78 +1,75 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.branches
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 branches controller for rhodecode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import tmpl_context as c
29 29 import binascii
30 30
31 31 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 32 from rhodecode.lib.base import BaseRepoController, render
33 33 from rhodecode.lib.compat import OrderedDict
34 34 from rhodecode.lib import safe_unicode
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class BranchesController(BaseRepoController):
39 39
40 40 @LoginRequired()
41 41 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
42 42 'repository.admin')
43 43 def __before__(self):
44 44 super(BranchesController, self).__before__()
45 45
46 46 def index(self):
47 47
48 48 def _branchtags(localrepo):
49
50 bt = {}
51 49 bt_closed = {}
52
53 50 for bn, heads in localrepo.branchmap().iteritems():
54 51 tip = heads[-1]
55 if 'close' not in localrepo.changelog.read(tip)[5]:
56 bt[bn] = tip
57 else:
52 if 'close' in localrepo.changelog.read(tip)[5]:
58 53 bt_closed[bn] = tip
59 return bt, bt_closed
54 return bt_closed
60 55
56 cs_g = c.rhodecode_repo.get_changeset
61 57
62 bt, bt_closed = _branchtags(c.rhodecode_repo._repo)
63 cs_g = c.rhodecode_repo.get_changeset
64 _branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in
65 bt.items()]
58 c.repo_closed_branches = {}
59 if c.rhodecode_db_repo.repo_type == 'hg':
60 bt_closed = _branchtags(c.rhodecode_repo._repo)
61 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),)
62 for n, h in bt_closed.items()]
66 63
67 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in
68 bt_closed.items()]
64 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
65 key=lambda ctx: ctx[0],
66 reverse=False))
69 67
68 _branches = [(safe_unicode(n), cs_g(h))
69 for n, h in c.rhodecode_repo.branches.items()]
70 70 c.repo_branches = OrderedDict(sorted(_branches,
71 71 key=lambda ctx: ctx[0],
72 72 reverse=False))
73 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
74 key=lambda ctx: ctx[0],
75 reverse=False))
76 73
77 74
78 75 return render('branches/branches.html')
@@ -1,116 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changelog
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changelog controller for rhodecode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 import traceback
27 28
28 29 from mercurial import graphmod
29 from pylons import request, session, tmpl_context as c
30 from pylons import request, url, session, tmpl_context as c
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
30 33
34 import rhodecode.lib.helpers as h
31 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 36 from rhodecode.lib.base import BaseRepoController, render
33 37 from rhodecode.lib.helpers import RepoPage
34 38 from rhodecode.lib.compat import json
35 39
40 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
41 from rhodecode.model.db import Repository
42
36 43 log = logging.getLogger(__name__)
37 44
38 45
39 46 class ChangelogController(BaseRepoController):
40 47
41 48 @LoginRequired()
42 49 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
43 50 'repository.admin')
44 51 def __before__(self):
45 52 super(ChangelogController, self).__before__()
46 53 c.affected_files_cut_off = 60
47 54
48 55 def index(self):
49 56 limit = 100
50 57 default = 20
51 58 if request.params.get('size'):
52 59 try:
53 60 int_size = int(request.params.get('size'))
54 61 except ValueError:
55 62 int_size = default
56 63 int_size = int_size if int_size <= limit else limit
57 64 c.size = int_size
58 65 session['changelog_size'] = c.size
59 66 session.save()
60 67 else:
61 68 c.size = int(session.get('changelog_size', default))
62 69
63 70 p = int(request.params.get('page', 1))
64 71 branch_name = request.params.get('branch', None)
65 c.total_cs = len(c.rhodecode_repo)
66 c.pagination = RepoPage(c.rhodecode_repo, page=p,
67 item_count=c.total_cs, items_per_page=c.size,
68 branch_name=branch_name)
72 try:
73 if branch_name:
74 collection = [z for z in
75 c.rhodecode_repo.get_changesets(start=0,
76 branch_name=branch_name)]
77 c.total_cs = len(collection)
78 else:
79 collection = c.rhodecode_repo
80 c.total_cs = len(c.rhodecode_repo)
69 81
70 self._graph(c.rhodecode_repo, c.total_cs, c.size, p)
82 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
83 items_per_page=c.size, branch=branch_name)
84 collection = list(c.pagination)
85 page_revisions = [x.raw_id for x in collection]
86 c.comments = c.rhodecode_db_repo.comments(page_revisions)
87
88 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
89 log.error(traceback.format_exc())
90 h.flash(str(e), category='warning')
91 return redirect(url('home'))
92
93 self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
94
95 c.branch_name = branch_name
96 c.branch_filters = [('', _('All Branches'))] + \
97 [(k, k) for k in c.rhodecode_repo.branches.keys()]
71 98
72 99 return render('changelog/changelog.html')
73 100
74 101 def changelog_details(self, cs):
75 102 if request.environ.get('HTTP_X_PARTIAL_XHR'):
76 103 c.cs = c.rhodecode_repo.get_changeset(cs)
77 104 return render('changelog/changelog_details.html')
78 105
79 def _graph(self, repo, repo_size, size, p):
106 def _graph(self, repo, collection, repo_size, size, p):
80 107 """
81 108 Generates a DAG graph for mercurial
82 109
83 110 :param repo: repo instance
84 111 :param size: number of commits to show
85 112 :param p: page number
86 113 """
87 if not repo.revisions:
114 if not collection:
88 115 c.jsdata = json.dumps([])
89 116 return
90 117
91 revcount = min(repo_size, size)
92 offset = 1 if p == 1 else ((p - 1) * revcount + 1)
93 try:
94 rev_end = repo.revisions.index(repo.revisions[(-1 * offset)])
95 except IndexError:
96 rev_end = repo.revisions.index(repo.revisions[-1])
97 rev_start = max(0, rev_end - revcount)
98
99 118 data = []
100 rev_end += 1
119 revs = [x.revision for x in collection]
101 120
102 121 if repo.alias == 'git':
103 for _ in xrange(rev_start, rev_end):
122 for _ in revs:
104 123 vtx = [0, 1]
105 124 edges = [[0, 0, 1]]
106 125 data.append(['', vtx, edges])
107 126
108 127 elif repo.alias == 'hg':
109 revs = list(reversed(xrange(rev_start, rev_end)))
110 128 c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs))
111 129 for (id, type, ctx, vtx, edges) in c.dag:
112 130 if type != graphmod.CHANGESET:
113 131 continue
114 132 data.append(['', vtx, edges])
115 133
116 134 c.jsdata = json.dumps(data)
@@ -1,252 +1,367 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 from collections import defaultdict
29 from webob.exc import HTTPForbidden
28 30
29 31 from pylons import tmpl_context as c, url, request, response
30 32 from pylons.i18n.translation import _
31 33 from pylons.controllers.util import redirect
34 from pylons.decorators import jsonify
35
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 ChangesetDoesNotExistError
38 from rhodecode.lib.vcs.nodes import FileNode
32 39
33 40 import rhodecode.lib.helpers as h
34 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 42 from rhodecode.lib.base import BaseRepoController, render
36 43 from rhodecode.lib.utils import EmptyChangeset
37 44 from rhodecode.lib.compat import OrderedDict
38
39 from vcs.exceptions import RepositoryError, ChangesetError, \
40 ChangesetDoesNotExistError
41 from vcs.nodes import FileNode
42 from vcs.utils import diffs as differ
45 from rhodecode.lib import diffs
46 from rhodecode.model.db import ChangesetComment
47 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.meta import Session
49 from rhodecode.lib.diffs import wrapped_diff
43 50
44 51 log = logging.getLogger(__name__)
45 52
46 53
54 def anchor_url(revision, path):
55 fid = h.FID(revision, path)
56 return h.url.current(anchor=fid, **request.GET)
57
58
59 def get_ignore_ws(fid, GET):
60 ig_ws_global = request.GET.get('ignorews')
61 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
62 if ig_ws:
63 try:
64 return int(ig_ws[0].split(':')[-1])
65 except:
66 pass
67 return ig_ws_global
68
69
70 def _ignorews_url(fileid=None):
71
72 params = defaultdict(list)
73 lbl = _('show white space')
74 ig_ws = get_ignore_ws(fileid, request.GET)
75 ln_ctx = get_line_ctx(fileid, request.GET)
76 # global option
77 if fileid is None:
78 if ig_ws is None:
79 params['ignorews'] += [1]
80 lbl = _('ignore white space')
81 ctx_key = 'context'
82 ctx_val = ln_ctx
83 # per file options
84 else:
85 if ig_ws is None:
86 params[fileid] += ['WS:1']
87 lbl = _('ignore white space')
88
89 ctx_key = fileid
90 ctx_val = 'C:%s' % ln_ctx
91 # if we have passed in ln_ctx pass it along to our params
92 if ln_ctx:
93 params[ctx_key] += [ctx_val]
94
95 params['anchor'] = fileid
96 img = h.image('/images/icons/text_strikethrough.png', lbl, class_='icon')
97 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
98
99
100 def get_line_ctx(fid, GET):
101 ln_ctx_global = request.GET.get('context')
102 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
103
104 if ln_ctx:
105 retval = ln_ctx[0].split(':')[-1]
106 else:
107 retval = ln_ctx_global
108
109 try:
110 return int(retval)
111 except:
112 return
113
114
115 def _context_url(fileid=None):
116 """
117 Generates url for context lines
118
119 :param fileid:
120 """
121 ig_ws = get_ignore_ws(fileid, request.GET)
122 ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2
123
124 params = defaultdict(list)
125
126 # global option
127 if fileid is None:
128 if ln_ctx > 0:
129 params['context'] += [ln_ctx]
130
131 if ig_ws:
132 ig_ws_key = 'ignorews'
133 ig_ws_val = 1
134
135 # per file option
136 else:
137 params[fileid] += ['C:%s' % ln_ctx]
138 ig_ws_key = fileid
139 ig_ws_val = 'WS:%s' % 1
140
141 if ig_ws:
142 params[ig_ws_key] += [ig_ws_val]
143
144 lbl = _('%s line context') % ln_ctx
145
146 params['anchor'] = fileid
147 img = h.image('/images/icons/table_add.png', lbl, class_='icon')
148 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
149
150
47 151 class ChangesetController(BaseRepoController):
48 152
49 153 @LoginRequired()
50 154 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
51 155 'repository.admin')
52 156 def __before__(self):
53 157 super(ChangesetController, self).__before__()
54 158 c.affected_files_cut_off = 60
55 159
56 160 def index(self, revision):
57 161
58 def wrap_to_table(str):
59
60 return '''<table class="code-difftable">
61 <tr class="line">
62 <td class="lineno new"></td>
63 <td class="code"><pre>%s</pre></td>
64 </tr>
65 </table>''' % str
162 c.anchor_url = anchor_url
163 c.ignorews_url = _ignorews_url
164 c.context_url = _context_url
66 165
67 166 #get ranges of revisions if preset
68 167 rev_range = revision.split('...')[:2]
69
168 enable_comments = True
70 169 try:
71 170 if len(rev_range) == 2:
171 enable_comments = False
72 172 rev_start = rev_range[0]
73 173 rev_end = rev_range[1]
74 174 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
75 175 end=rev_end)
76 176 else:
77 177 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
78 178
79 179 c.cs_ranges = list(rev_ranges)
180 if not c.cs_ranges:
181 raise RepositoryError('Changeset range returned empty result')
80 182
81 183 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
82 184 log.error(traceback.format_exc())
83 185 h.flash(str(e), category='warning')
84 186 return redirect(url('home'))
85 187
86 188 c.changes = OrderedDict()
87 c.sum_added = 0
88 c.sum_removed = 0
89 c.lines_added = 0
90 c.lines_deleted = 0
189
190 c.lines_added = 0 # count of lines added
191 c.lines_deleted = 0 # count of lines removes
192
193 cumulative_diff = 0
91 194 c.cut_off = False # defines if cut off limit is reached
92 195
196 c.comments = []
197 c.inline_comments = []
198 c.inline_cnt = 0
93 199 # Iterate over ranges (default changeset view is always one changeset)
94 200 for changeset in c.cs_ranges:
201 c.comments.extend(ChangesetCommentsModel()\
202 .get_comments(c.rhodecode_db_repo.repo_id,
203 changeset.raw_id))
204 inlines = ChangesetCommentsModel()\
205 .get_inline_comments(c.rhodecode_db_repo.repo_id,
206 changeset.raw_id)
207 c.inline_comments.extend(inlines)
95 208 c.changes[changeset.raw_id] = []
96 209 try:
97 210 changeset_parent = changeset.parents[0]
98 211 except IndexError:
99 212 changeset_parent = None
100 213
101 214 #==================================================================
102 215 # ADDED FILES
103 216 #==================================================================
104 217 for node in changeset.added:
105
106 filenode_old = FileNode(node.path, '', EmptyChangeset())
107 if filenode_old.is_binary or node.is_binary:
108 diff = wrap_to_table(_('binary file'))
109 st = (0, 0)
110 else:
111 # in this case node.size is good parameter since those are
112 # added nodes and their size defines how many changes were
113 # made
114 c.sum_added += node.size
115 if c.sum_added < self.cut_off_limit:
116 f_gitdiff = differ.get_gitdiff(filenode_old, node)
117 d = differ.DiffProcessor(f_gitdiff, format='gitdiff')
118
119 st = d.stat()
120 diff = d.as_html()
121
122 else:
123 diff = wrap_to_table(_('Changeset is to big and '
124 'was cut off, see raw '
125 'changeset instead'))
126 c.cut_off = True
127 break
128
129 cs1 = None
130 cs2 = node.last_changeset.raw_id
218 fid = h.FID(revision, node.path)
219 line_context_lcl = get_line_ctx(fid, request.GET)
220 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
221 lim = self.cut_off_limit
222 if cumulative_diff > self.cut_off_limit:
223 lim = -1
224 size, cs1, cs2, diff, st = wrapped_diff(filenode_old=None,
225 filenode_new=node,
226 cut_off_limit=lim,
227 ignore_whitespace=ign_whitespace_lcl,
228 line_context=line_context_lcl,
229 enable_comments=enable_comments)
230 cumulative_diff += size
131 231 c.lines_added += st[0]
132 232 c.lines_deleted += st[1]
133 233 c.changes[changeset.raw_id].append(('added', node, diff,
134 234 cs1, cs2, st))
135 235
136 236 #==================================================================
137 237 # CHANGED FILES
138 238 #==================================================================
139 if not c.cut_off:
140 for node in changeset.changed:
141 try:
142 filenode_old = changeset_parent.get_node(node.path)
143 except ChangesetError:
144 log.warning('Unable to fetch parent node for diff')
145 filenode_old = FileNode(node.path, '',
146 EmptyChangeset())
147
148 if filenode_old.is_binary or node.is_binary:
149 diff = wrap_to_table(_('binary file'))
150 st = (0, 0)
151 else:
239 for node in changeset.changed:
240 try:
241 filenode_old = changeset_parent.get_node(node.path)
242 except ChangesetError:
243 log.warning('Unable to fetch parent node for diff')
244 filenode_old = FileNode(node.path, '', EmptyChangeset())
152 245
153 if c.sum_removed < self.cut_off_limit:
154 f_gitdiff = differ.get_gitdiff(filenode_old, node)
155 d = differ.DiffProcessor(f_gitdiff,
156 format='gitdiff')
157 st = d.stat()
158 if (st[0] + st[1]) * 256 > self.cut_off_limit:
159 diff = wrap_to_table(_('Diff is to big '
160 'and was cut off, see '
161 'raw diff instead'))
162 else:
163 diff = d.as_html()
164
165 if diff:
166 c.sum_removed += len(diff)
167 else:
168 diff = wrap_to_table(_('Changeset is to big and '
169 'was cut off, see raw '
170 'changeset instead'))
171 c.cut_off = True
172 break
173
174 cs1 = filenode_old.last_changeset.raw_id
175 cs2 = node.last_changeset.raw_id
176 c.lines_added += st[0]
177 c.lines_deleted += st[1]
178 c.changes[changeset.raw_id].append(('changed', node, diff,
179 cs1, cs2, st))
246 fid = h.FID(revision, node.path)
247 line_context_lcl = get_line_ctx(fid, request.GET)
248 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
249 lim = self.cut_off_limit
250 if cumulative_diff > self.cut_off_limit:
251 lim = -1
252 size, cs1, cs2, diff, st = wrapped_diff(filenode_old=filenode_old,
253 filenode_new=node,
254 cut_off_limit=lim,
255 ignore_whitespace=ign_whitespace_lcl,
256 line_context=line_context_lcl,
257 enable_comments=enable_comments)
258 cumulative_diff += size
259 c.lines_added += st[0]
260 c.lines_deleted += st[1]
261 c.changes[changeset.raw_id].append(('changed', node, diff,
262 cs1, cs2, st))
180 263
181 264 #==================================================================
182 265 # REMOVED FILES
183 266 #==================================================================
184 if not c.cut_off:
185 for node in changeset.removed:
186 c.changes[changeset.raw_id].append(('removed', node, None,
187 None, None, (0, 0)))
267 for node in changeset.removed:
268 c.changes[changeset.raw_id].append(('removed', node, None,
269 None, None, (0, 0)))
270
271 # count inline comments
272 for path, lines in c.inline_comments:
273 for comments in lines.values():
274 c.inline_cnt += len(comments)
188 275
189 276 if len(c.cs_ranges) == 1:
190 277 c.changeset = c.cs_ranges[0]
191 278 c.changes = c.changes[c.changeset.raw_id]
192 279
193 280 return render('changeset/changeset.html')
194 281 else:
195 282 return render('changeset/changeset_range.html')
196 283
197 284 def raw_changeset(self, revision):
198 285
199 286 method = request.GET.get('diff', 'show')
287 ignore_whitespace = request.GET.get('ignorews') == '1'
288 line_context = request.GET.get('context', 3)
200 289 try:
201 290 c.scm_type = c.rhodecode_repo.alias
202 291 c.changeset = c.rhodecode_repo.get_changeset(revision)
203 292 except RepositoryError:
204 293 log.error(traceback.format_exc())
205 294 return redirect(url('home'))
206 295 else:
207 296 try:
208 297 c.changeset_parent = c.changeset.parents[0]
209 298 except IndexError:
210 299 c.changeset_parent = None
211 300 c.changes = []
212 301
213 302 for node in c.changeset.added:
214 303 filenode_old = FileNode(node.path, '')
215 304 if filenode_old.is_binary or node.is_binary:
216 305 diff = _('binary file') + '\n'
217 306 else:
218 f_gitdiff = differ.get_gitdiff(filenode_old, node)
219 diff = differ.DiffProcessor(f_gitdiff,
307 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
308 ignore_whitespace=ignore_whitespace,
309 context=line_context)
310 diff = diffs.DiffProcessor(f_gitdiff,
220 311 format='gitdiff').raw_diff()
221 312
222 313 cs1 = None
223 314 cs2 = node.last_changeset.raw_id
224 315 c.changes.append(('added', node, diff, cs1, cs2))
225 316
226 317 for node in c.changeset.changed:
227 318 filenode_old = c.changeset_parent.get_node(node.path)
228 319 if filenode_old.is_binary or node.is_binary:
229 320 diff = _('binary file')
230 321 else:
231 f_gitdiff = differ.get_gitdiff(filenode_old, node)
232 diff = differ.DiffProcessor(f_gitdiff,
322 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
323 ignore_whitespace=ignore_whitespace,
324 context=line_context)
325 diff = diffs.DiffProcessor(f_gitdiff,
233 326 format='gitdiff').raw_diff()
234 327
235 328 cs1 = filenode_old.last_changeset.raw_id
236 329 cs2 = node.last_changeset.raw_id
237 330 c.changes.append(('changed', node, diff, cs1, cs2))
238 331
239 332 response.content_type = 'text/plain'
240 333
241 334 if method == 'download':
242 335 response.content_disposition = 'attachment; filename=%s.patch' \
243 336 % revision
244 337
245 338 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
246 339 c.changeset.parents])
247 340
248 341 c.diffs = ''
249 342 for x in c.changes:
250 343 c.diffs += x[2]
251 344
252 345 return render('changeset/raw_changeset.html')
346
347 def comment(self, repo_name, revision):
348 ChangesetCommentsModel().create(text=request.POST.get('text'),
349 repo_id=c.rhodecode_db_repo.repo_id,
350 user_id=c.rhodecode_user.user_id,
351 revision=revision,
352 f_path=request.POST.get('f_path'),
353 line_no=request.POST.get('line'))
354 Session.commit()
355 return redirect(h.url('changeset_home', repo_name=repo_name,
356 revision=revision))
357
358 @jsonify
359 def delete_comment(self, repo_name, comment_id):
360 co = ChangesetComment.get(comment_id)
361 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
362 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
363 ChangesetCommentsModel().delete(comment=co)
364 Session.commit()
365 return True
366 else:
367 raise HTTPForbidden()
@@ -1,108 +1,108 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.error
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode error controller
7 7
8 8 :created_on: Dec 8, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import cgi
27 27 import logging
28 28 import paste.fileapp
29 29
30 30 from pylons import tmpl_context as c, request, config, url
31 31 from pylons.i18n.translation import _
32 32 from pylons.middleware import media_path
33 33
34 34 from rhodecode.lib.base import BaseController, render
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class ErrorController(BaseController):
40 40 """Generates error documents as and when they are required.
41 41
42 42 The ErrorDocuments middleware forwards to ErrorController when error
43 43 related status codes are returned from the application.
44 44
45 45 This behavior can be altered by changing the parameters to the
46 46 ErrorDocuments middleware in your config/middleware.py file.
47 47 """
48 48
49 49 def __before__(self):
50 50 #disable all base actions since we don't need them here
51 51 pass
52 52
53 53 def document(self):
54 54 resp = request.environ.get('pylons.original_response')
55 55 c.rhodecode_name = config.get('rhodecode_title')
56 56
57 log.debug('### %s ###', resp.status)
57 log.debug('### %s ###' % resp.status)
58 58
59 59 e = request.environ
60 60 c.serv_p = r'%(protocol)s://%(host)s/' \
61 61 % {'protocol': e.get('wsgi.url_scheme'),
62 62 'host': e.get('HTTP_HOST'), }
63 63
64 64 c.error_message = cgi.escape(request.GET.get('code', str(resp.status)))
65 65 c.error_explanation = self.get_error_explanation(resp.status_int)
66 66
67 67 # redirect to when error with given seconds
68 68 c.redirect_time = 0
69 69 c.redirect_module = _('Home page')
70 70 c.url_redirect = "/"
71 71
72 72 return render('/errors/error_document.html')
73 73
74 74 def img(self, id):
75 75 """Serve Pylons' stock images"""
76 76 return self._serve_file(os.path.join(media_path, 'img', id))
77 77
78 78 def style(self, id):
79 79 """Serve Pylons' stock stylesheets"""
80 80 return self._serve_file(os.path.join(media_path, 'style', id))
81 81
82 82 def _serve_file(self, path):
83 83 """Call Paste's FileApp (a WSGI application) to serve the file
84 84 at the specified path
85 85 """
86 86 fapp = paste.fileapp.FileApp(path)
87 87 return fapp(request.environ, self.start_response)
88 88
89 89 def get_error_explanation(self, code):
90 90 ''' get the error explanations of int codes
91 91 [400, 401, 403, 404, 500]'''
92 92 try:
93 93 code = int(code)
94 94 except:
95 95 code = 500
96 96
97 97 if code == 400:
98 98 return _('The request could not be understood by the server'
99 99 ' due to malformed syntax.')
100 100 if code == 401:
101 101 return _('Unauthorized access to resource')
102 102 if code == 403:
103 103 return _("You don't have permission to view this page")
104 104 if code == 404:
105 105 return _('The resource could not be found')
106 106 if code == 500:
107 107 return _('The server encountered an unexpected condition'
108 108 ' which prevented it from fulfilling the request.')
@@ -1,116 +1,127 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.feed
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Feed controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 31 from rhodecode.lib import safe_unicode
32 32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 33 from rhodecode.lib.base import BaseRepoController
34 34
35 35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class FeedController(BaseRepoController):
41 41
42 42 @LoginRequired(api_access=True)
43 43 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 44 'repository.admin')
45 45 def __before__(self):
46 46 super(FeedController, self).__before__()
47 47 #common values for feeds
48 48 self.description = _('Changes on %s repository')
49 49 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
50 50 self.language = 'en-us'
51 51 self.ttl = "5"
52 52 self.feed_nr = 10
53 53
54 def _get_title(self, cs):
55 return "R%s:%s - %s" % (
56 cs.revision, cs.short_id, cs.message
57 )
58
54 59 def __changes(self, cs):
55 60 changes = []
56 61
57 62 a = [safe_unicode(n.path) for n in cs.added]
58 63 if a:
59 64 changes.append('\nA ' + '\nA '.join(a))
60 65
61 66 m = [safe_unicode(n.path) for n in cs.changed]
62 67 if m:
63 68 changes.append('\nM ' + '\nM '.join(m))
64 69
65 70 d = [safe_unicode(n.path) for n in cs.removed]
66 71 if d:
67 72 changes.append('\nD ' + '\nD '.join(d))
68 73
69 74 changes.append('</pre>')
70 75
71 76 return ''.join(changes)
72 77
73 78 def atom(self, repo_name):
74 79 """Produce an atom-1.0 feed via feedgenerator module"""
75 feed = Atom1Feed(title=self.title % repo_name,
76 link=url('summary_home', repo_name=repo_name,
77 qualified=True),
78 description=self.description % repo_name,
79 language=self.language,
80 ttl=self.ttl)
81 desc_msg = []
80 feed = Atom1Feed(
81 title=self.title % repo_name,
82 link=url('summary_home', repo_name=repo_name,
83 qualified=True),
84 description=self.description % repo_name,
85 language=self.language,
86 ttl=self.ttl
87 )
88
82 89 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
90 desc_msg = []
83 91 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
84 92 desc_msg.append(self.__changes(cs))
85 93
86 feed.add_item(title=cs.message,
94 feed.add_item(title=self._get_title(cs),
87 95 link=url('changeset_home', repo_name=repo_name,
88 96 revision=cs.raw_id, qualified=True),
89 97 author_name=cs.author,
90 98 description=''.join(desc_msg))
91 99
92 100 response.content_type = feed.mime_type
93 101 return feed.writeString('utf-8')
94 102
95 103 def rss(self, repo_name):
96 104 """Produce an rss2 feed via feedgenerator module"""
97 feed = Rss201rev2Feed(title=self.title % repo_name,
98 link=url('summary_home', repo_name=repo_name,
99 qualified=True),
100 description=self.description % repo_name,
101 language=self.language,
102 ttl=self.ttl)
103 desc_msg = []
105 feed = Rss201rev2Feed(
106 title=self.title % repo_name,
107 link=url('summary_home', repo_name=repo_name,
108 qualified=True),
109 description=self.description % repo_name,
110 language=self.language,
111 ttl=self.ttl
112 )
113
104 114 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
115 desc_msg = []
105 116 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
106 117 desc_msg.append(self.__changes(cs))
107 118
108 feed.add_item(title=cs.message,
119 feed.add_item(title=self._get_title(cs),
109 120 link=url('changeset_home', repo_name=repo_name,
110 121 revision=cs.raw_id, qualified=True),
111 122 author_name=cs.author,
112 123 description=''.join(desc_msg),
113 124 )
114 125
115 126 response.content_type = feed.mime_type
116 127 return feed.writeString('utf-8')
@@ -1,513 +1,492 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import traceback
29 29
30 from os.path import join as jn
31
32 from pylons import request, response, session, tmpl_context as c, url
30 from pylons import request, response, tmpl_context as c, url
33 31 from pylons.i18n.translation import _
34 32 from pylons.controllers.util import redirect
35 33 from pylons.decorators import jsonify
36 34
37 from vcs.conf import settings
38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
40 from vcs.nodes import FileNode, NodeKind
41 from vcs.utils import diffs as differ
35 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
38 NodeAlreadyExistsError
39 from rhodecode.lib.vcs.nodes import FileNode
42 40
41 from rhodecode.lib.compat import OrderedDict
43 42 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
44 43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 44 from rhodecode.lib.base import BaseRepoController, render
46 45 from rhodecode.lib.utils import EmptyChangeset
46 from rhodecode.lib import diffs
47 47 import rhodecode.lib.helpers as h
48 48 from rhodecode.model.repo import RepoModel
49 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
50 _context_url, get_line_ctx, get_ignore_ws
51 from rhodecode.lib.diffs import wrapped_diff
52 from rhodecode.model.scm import ScmModel
49 53
50 54 log = logging.getLogger(__name__)
51 55
52 56
53 57 class FilesController(BaseRepoController):
54 58
55 59 @LoginRequired()
56 60 def __before__(self):
57 61 super(FilesController, self).__before__()
58 62 c.cut_off_limit = self.cut_off_limit
59 63
60 64 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
61 65 """
62 66 Safe way to get changeset if error occur it redirects to tip with
63 67 proper message
64 68
65 69 :param rev: revision to fetch
66 70 :param repo_name: repo name to redirect after
67 71 """
68 72
69 73 try:
70 74 return c.rhodecode_repo.get_changeset(rev)
71 75 except EmptyRepositoryError, e:
72 76 if not redirect_after:
73 77 return None
74 78 url_ = url('files_add_home',
75 79 repo_name=c.repo_name,
76 80 revision=0, f_path='')
77 81 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
78 82 h.flash(h.literal(_('There are no files yet %s' % add_new)),
79 83 category='warning')
80 84 redirect(h.url('summary_home', repo_name=repo_name))
81 85
82 86 except RepositoryError, e:
83 87 h.flash(str(e), category='warning')
84 88 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
85 89
86 90 def __get_filenode_or_redirect(self, repo_name, cs, path):
87 91 """
88 92 Returns file_node, if error occurs or given path is directory,
89 93 it'll redirect to top level path
90 94
91 95 :param repo_name: repo_name
92 96 :param cs: given changeset
93 97 :param path: path to lookup
94 98 """
95 99
96 100 try:
97 101 file_node = cs.get_node(path)
98 102 if file_node.is_dir():
99 103 raise RepositoryError('given path is a directory')
100 104 except RepositoryError, e:
101 105 h.flash(str(e), category='warning')
102 106 redirect(h.url('files_home', repo_name=repo_name,
103 107 revision=cs.raw_id))
104 108
105 109 return file_node
106 110
107
108 def __get_paths(self, changeset, starting_path):
109 """recursive walk in root dir and return a set of all path in that dir
110 based on repository walk function
111 """
112 _files = list()
113 _dirs = list()
114
115 try:
116 tip = changeset
117 for topnode, dirs, files in tip.walk(starting_path):
118 for f in files:
119 _files.append(f.path)
120 for d in dirs:
121 _dirs.append(d.path)
122 except RepositoryError, e:
123 log.debug(traceback.format_exc())
124 pass
125 return _dirs, _files
126
127 111 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
128 112 'repository.admin')
129 113 def index(self, repo_name, revision, f_path):
130 114 # redirect to given revision from form if given
131 115 post_revision = request.POST.get('at_rev', None)
132 116 if post_revision:
133 117 cs = self.__get_cs_or_redirect(post_revision, repo_name)
134 118 redirect(url('files_home', repo_name=c.repo_name,
135 119 revision=cs.raw_id, f_path=f_path))
136 120
137 121 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
138 122 c.branch = request.GET.get('branch', None)
139 123 c.f_path = f_path
140 124
141 125 cur_rev = c.changeset.revision
142 126
143 127 # prev link
144 128 try:
145 129 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
146 130 c.url_prev = url('files_home', repo_name=c.repo_name,
147 131 revision=prev_rev.raw_id, f_path=f_path)
148 132 if c.branch:
149 133 c.url_prev += '?branch=%s' % c.branch
150 134 except (ChangesetDoesNotExistError, VCSError):
151 135 c.url_prev = '#'
152 136
153 137 # next link
154 138 try:
155 139 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
156 140 c.url_next = url('files_home', repo_name=c.repo_name,
157 141 revision=next_rev.raw_id, f_path=f_path)
158 142 if c.branch:
159 143 c.url_next += '?branch=%s' % c.branch
160 144 except (ChangesetDoesNotExistError, VCSError):
161 145 c.url_next = '#'
162 146
163 147 # files or dirs
164 148 try:
165 c.files_list = c.changeset.get_node(f_path)
149 c.file = c.changeset.get_node(f_path)
166 150
167 if c.files_list.is_file():
151 if c.file.is_file():
168 152 c.file_history = self._get_node_history(c.changeset, f_path)
169 153 else:
170 154 c.file_history = []
171 155 except RepositoryError, e:
172 156 h.flash(str(e), category='warning')
173 157 redirect(h.url('files_home', repo_name=repo_name,
174 158 revision=revision))
175 159
176 160 return render('files/files.html')
177 161
178 162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
179 163 'repository.admin')
180 164 def rawfile(self, repo_name, revision, f_path):
181 165 cs = self.__get_cs_or_redirect(revision, repo_name)
182 166 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
183 167
184 168 response.content_disposition = 'attachment; filename=%s' % \
185 169 safe_str(f_path.split(os.sep)[-1])
186 170
187 171 response.content_type = file_node.mimetype
188 172 return file_node.content
189 173
190 174 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
191 175 'repository.admin')
192 176 def raw(self, repo_name, revision, f_path):
193 177 cs = self.__get_cs_or_redirect(revision, repo_name)
194 178 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
195 179
196 180 raw_mimetype_mapping = {
197 181 # map original mimetype to a mimetype used for "show as raw"
198 182 # you can also provide a content-disposition to override the
199 183 # default "attachment" disposition.
200 184 # orig_type: (new_type, new_dispo)
201 185
202 186 # show images inline:
203 187 'image/x-icon': ('image/x-icon', 'inline'),
204 188 'image/png': ('image/png', 'inline'),
205 189 'image/gif': ('image/gif', 'inline'),
206 190 'image/jpeg': ('image/jpeg', 'inline'),
207 191 'image/svg+xml': ('image/svg+xml', 'inline'),
208 192 }
209 193
210 194 mimetype = file_node.mimetype
211 195 try:
212 196 mimetype, dispo = raw_mimetype_mapping[mimetype]
213 197 except KeyError:
214 198 # we don't know anything special about this, handle it safely
215 199 if file_node.is_binary:
216 200 # do same as download raw for binary files
217 201 mimetype, dispo = 'application/octet-stream', 'attachment'
218 202 else:
219 203 # do not just use the original mimetype, but force text/plain,
220 204 # otherwise it would serve text/html and that might be unsafe.
221 205 # Note: underlying vcs library fakes text/plain mimetype if the
222 206 # mimetype can not be determined and it thinks it is not
223 207 # binary.This might lead to erroneous text display in some
224 208 # cases, but helps in other cases, like with text files
225 209 # without extension.
226 210 mimetype, dispo = 'text/plain', 'inline'
227 211
228 212 if dispo == 'attachment':
229 213 dispo = 'attachment; filename=%s' % \
230 214 safe_str(f_path.split(os.sep)[-1])
231 215
232 216 response.content_disposition = dispo
233 217 response.content_type = mimetype
234 218 return file_node.content
235 219
236 220 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
237 221 'repository.admin')
238 222 def annotate(self, repo_name, revision, f_path):
239 223 c.cs = self.__get_cs_or_redirect(revision, repo_name)
240 224 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
241 225
242 226 c.file_history = self._get_node_history(c.cs, f_path)
243 227 c.f_path = f_path
244 228 return render('files/files_annotate.html')
245 229
246 230 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
247 231 def edit(self, repo_name, revision, f_path):
248 232 r_post = request.POST
249 233
250 234 c.cs = self.__get_cs_or_redirect(revision, repo_name)
251 235 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
252 236
253 237 if c.file.is_binary:
254 238 return redirect(url('files_home', repo_name=c.repo_name,
255 239 revision=c.cs.raw_id, f_path=f_path))
256 240
257 241 c.f_path = f_path
258 242
259 243 if r_post:
260 244
261 245 old_content = c.file.content
262 246 sl = old_content.splitlines(1)
263 247 first_line = sl[0] if sl else ''
264 248 # modes: 0 - Unix, 1 - Mac, 2 - DOS
265 249 mode = detect_mode(first_line, 0)
266 250 content = convert_line_endings(r_post.get('content'), mode)
267 251
268 252 message = r_post.get('message') or (_('Edited %s via RhodeCode')
269 253 % (f_path))
270 254 author = self.rhodecode_user.full_contact
271 255
272 256 if content == old_content:
273 257 h.flash(_('No changes'),
274 258 category='warning')
275 259 return redirect(url('changeset_home', repo_name=c.repo_name,
276 260 revision='tip'))
277 261
278 262 try:
279 263 self.scm_model.commit_change(repo=c.rhodecode_repo,
280 264 repo_name=repo_name, cs=c.cs,
281 265 user=self.rhodecode_user,
282 266 author=author, message=message,
283 267 content=content, f_path=f_path)
284 268 h.flash(_('Successfully committed to %s' % f_path),
285 269 category='success')
286 270
287 271 except Exception:
288 272 log.error(traceback.format_exc())
289 273 h.flash(_('Error occurred during commit'), category='error')
290 274 return redirect(url('changeset_home',
291 275 repo_name=c.repo_name, revision='tip'))
292 276
293 277 return render('files/files_edit.html')
294 278
295 279 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
296 280 def add(self, repo_name, revision, f_path):
297 281 r_post = request.POST
298 282 c.cs = self.__get_cs_or_redirect(revision, repo_name,
299 283 redirect_after=False)
300 284 if c.cs is None:
301 285 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
302 286
303 287 c.f_path = f_path
304 288
305 289 if r_post:
306 290 unix_mode = 0
307 291 content = convert_line_endings(r_post.get('content'), unix_mode)
308 292
309 293 message = r_post.get('message') or (_('Added %s via RhodeCode')
310 294 % (f_path))
311 295 location = r_post.get('location')
312 296 filename = r_post.get('filename')
313 297 file_obj = r_post.get('upload_file', None)
314 298
315 299 if file_obj is not None and hasattr(file_obj, 'filename'):
316 300 filename = file_obj.filename
317 301 content = file_obj.file
318 302
319 303 node_path = os.path.join(location, filename)
320 304 author = self.rhodecode_user.full_contact
321 305
322 306 if not content:
323 307 h.flash(_('No content'), category='warning')
324 308 return redirect(url('changeset_home', repo_name=c.repo_name,
325 309 revision='tip'))
326 310 if not filename:
327 311 h.flash(_('No filename'), category='warning')
328 312 return redirect(url('changeset_home', repo_name=c.repo_name,
329 313 revision='tip'))
330 314
331 315 try:
332 316 self.scm_model.create_node(repo=c.rhodecode_repo,
333 317 repo_name=repo_name, cs=c.cs,
334 318 user=self.rhodecode_user,
335 319 author=author, message=message,
336 320 content=content, f_path=node_path)
337 321 h.flash(_('Successfully committed to %s' % node_path),
338 322 category='success')
339 323 except NodeAlreadyExistsError, e:
340 324 h.flash(_(e), category='error')
341 325 except Exception:
342 326 log.error(traceback.format_exc())
343 327 h.flash(_('Error occurred during commit'), category='error')
344 328 return redirect(url('changeset_home',
345 329 repo_name=c.repo_name, revision='tip'))
346 330
347 331 return render('files/files_add.html')
348 332
349 333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
350 334 'repository.admin')
351 335 def archivefile(self, repo_name, fname):
352 336
353 337 fileformat = None
354 338 revision = None
355 339 ext = None
356 340 subrepos = request.GET.get('subrepos') == 'true'
357 341
358 342 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
359 343 archive_spec = fname.split(ext_data[1])
360 344 if len(archive_spec) == 2 and archive_spec[1] == '':
361 345 fileformat = a_type or ext_data[1]
362 346 revision = archive_spec[0]
363 347 ext = ext_data[1]
364 348
365 349 try:
366 350 dbrepo = RepoModel().get_by_repo_name(repo_name)
367 351 if dbrepo.enable_downloads is False:
368 352 return _('downloads disabled')
369 353
370 354 if c.rhodecode_repo.alias == 'hg':
371 355 # patch and reset hooks section of UI config to not run any
372 356 # hooks on fetching archives with subrepos
373 357 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
374 358 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
375 359
376 360 cs = c.rhodecode_repo.get_changeset(revision)
377 361 content_type = settings.ARCHIVE_SPECS[fileformat][0]
378 362 except ChangesetDoesNotExistError:
379 363 return _('Unknown revision %s') % revision
380 364 except EmptyRepositoryError:
381 365 return _('Empty repository')
382 366 except (ImproperArchiveTypeError, KeyError):
383 367 return _('Unknown archive type')
384 368
385 369 response.content_type = content_type
386 370 response.content_disposition = 'attachment; filename=%s-%s%s' \
387 371 % (repo_name, revision, ext)
388 372
389 373 import tempfile
390 374 archive = tempfile.mkstemp()[1]
391 375 t = open(archive, 'wb')
392 376 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
393 377
394 378 def get_chunked_archive(archive):
395 379 stream = open(archive, 'rb')
396 380 while True:
397 381 data = stream.read(4096)
398 382 if not data:
399 383 os.remove(archive)
400 384 break
401 385 yield data
402 386
403 387 return get_chunked_archive(archive)
404 388
405 389 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
406 390 'repository.admin')
407 391 def diff(self, repo_name, f_path):
408 diff1 = request.GET.get('diff1')
409 diff2 = request.GET.get('diff2')
392 ignore_whitespace = request.GET.get('ignorews') == '1'
393 line_context = request.GET.get('context', 3)
394 diff1 = request.GET.get('diff1', '')
395 diff2 = request.GET.get('diff2', '')
410 396 c.action = request.GET.get('diff')
411 397 c.no_changes = diff1 == diff2
412 398 c.f_path = f_path
413 399 c.big_diff = False
414
400 c.anchor_url = anchor_url
401 c.ignorews_url = _ignorews_url
402 c.context_url = _context_url
403 c.changes = OrderedDict()
404 c.changes[diff2] = []
415 405 try:
416 406 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
417 407 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
418 408 node1 = c.changeset_1.get_node(f_path)
419 409 else:
420 410 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
421 411 node1 = FileNode('.', '', changeset=c.changeset_1)
422 412
423 413 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
424 414 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
425 415 node2 = c.changeset_2.get_node(f_path)
426 416 else:
427 417 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
428 418 node2 = FileNode('.', '', changeset=c.changeset_2)
429 419 except RepositoryError:
430 return redirect(url('files_home',
431 repo_name=c.repo_name, f_path=f_path))
420 return redirect(url('files_home', repo_name=c.repo_name,
421 f_path=f_path))
432 422
433 423 if c.action == 'download':
434 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
435 format='gitdiff')
424 _diff = diffs.get_gitdiff(node1, node2,
425 ignore_whitespace=ignore_whitespace,
426 context=line_context)
427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
436 428
437 429 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
438 430 response.content_type = 'text/plain'
439 431 response.content_disposition = 'attachment; filename=%s' \
440 432 % diff_name
441 433 return diff.raw_diff()
442 434
443 435 elif c.action == 'raw':
444 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
445 format='gitdiff')
436 _diff = diffs.get_gitdiff(node1, node2,
437 ignore_whitespace=ignore_whitespace,
438 context=line_context)
439 diff = diffs.DiffProcessor(_diff, format='gitdiff')
446 440 response.content_type = 'text/plain'
447 441 return diff.raw_diff()
448 442
449 elif c.action == 'diff':
450 if node1.is_binary or node2.is_binary:
451 c.cur_diff = _('Binary file')
452 elif node1.size > self.cut_off_limit or \
453 node2.size > self.cut_off_limit:
454 c.cur_diff = ''
455 c.big_diff = True
456 else:
457 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
458 format='gitdiff')
459 c.cur_diff = diff.as_html()
460 443 else:
444 fid = h.FID(diff2, node2.path)
445 line_context_lcl = get_line_ctx(fid, request.GET)
446 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
461 447
462 #default option
463 if node1.is_binary or node2.is_binary:
464 c.cur_diff = _('Binary file')
465 elif node1.size > self.cut_off_limit or \
466 node2.size > self.cut_off_limit:
467 c.cur_diff = ''
468 c.big_diff = True
448 lim = request.GET.get('fulldiff') or self.cut_off_limit
449 _, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1,
450 filenode_new=node2,
451 cut_off_limit=lim,
452 ignore_whitespace=ign_whitespace_lcl,
453 line_context=line_context_lcl,
454 enable_comments=False)
469 455
470 else:
471 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
472 format='gitdiff')
473 c.cur_diff = diff.as_html()
456 c.changes = [('', node2, diff, cs1, cs2, st,)]
474 457
475 if not c.cur_diff and not c.big_diff:
476 c.no_changes = True
477 458 return render('files/file_diff.html')
478 459
479 460 def _get_node_history(self, cs, f_path):
480 461 changesets = cs.get_file_history(f_path)
481 462 hist_l = []
482 463
483 464 changesets_group = ([], _("Changesets"))
484 465 branches_group = ([], _("Branches"))
485 466 tags_group = ([], _("Tags"))
486 467
487 468 for chs in changesets:
488 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
469 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, chs.branch)
489 470 changesets_group[0].append((chs.raw_id, n_desc,))
490 471
491 472 hist_l.append(changesets_group)
492 473
493 474 for name, chs in c.rhodecode_repo.branches.items():
494 #chs = chs.split(':')[-1]
495 475 branches_group[0].append((chs, name),)
496 476 hist_l.append(branches_group)
497 477
498 478 for name, chs in c.rhodecode_repo.tags.items():
499 #chs = chs.split(':')[-1]
500 479 tags_group[0].append((chs, name),)
501 480 hist_l.append(tags_group)
502 481
503 482 return hist_l
504 483
505 484 @jsonify
506 485 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
507 486 'repository.admin')
508 487 def nodelist(self, repo_name, revision, f_path):
509 488 if request.environ.get('HTTP_X_PARTIAL_XHR'):
510 489 cs = self.__get_cs_or_redirect(revision, repo_name)
511 _d, _f = self.__get_paths(cs, f_path)
490 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
491 flat=False)
512 492 return _d + _f
513
@@ -1,57 +1,57 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.followers
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Followers controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26
27 27 from pylons import tmpl_context as c, request
28 28
29 29 from rhodecode.lib.helpers import Page
30 30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
31 31 from rhodecode.lib.base import BaseRepoController, render
32 32 from rhodecode.model.db import Repository, User, UserFollowing
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class FollowersController(BaseRepoController):
38 38
39 39 @LoginRequired()
40 40 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 41 'repository.admin')
42 42 def __before__(self):
43 43 super(FollowersController, self).__before__()
44 44
45 45 def followers(self, repo_name):
46 46 p = int(request.params.get('page', 1))
47 47 repo_id = c.rhodecode_db_repo.repo_id
48 48 d = UserFollowing.get_repo_followers(repo_id)\
49 49 .order_by(UserFollowing.follows_from)
50 50 c.followers_pager = Page(d, page=p, items_per_page=20)
51 51
52 52 c.followers_data = render('/followers/followers_data.html')
53 53
54 54 if request.environ.get('HTTP_X_PARTIAL_XHR'):
55 55 return c.followers_data
56 56
57 57 return render('/followers/followers.html')
@@ -1,56 +1,174 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.forks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 forks controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 import formencode
27 import traceback
28 from formencode import htmlfill
26 29
27 from pylons import tmpl_context as c, request
30 from pylons import tmpl_context as c, request, url
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
33
34 import rhodecode.lib.helpers as h
28 35
29 36 from rhodecode.lib.helpers import Page
30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
38 NotAnonymous
31 39 from rhodecode.lib.base import BaseRepoController, render
32 from rhodecode.model.db import Repository, User, UserFollowing
40 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
41 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.forms import RepoForkForm
33 43
34 44 log = logging.getLogger(__name__)
35 45
36 46
37 47 class ForksController(BaseRepoController):
38 48
39 49 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 'repository.admin')
42 50 def __before__(self):
43 51 super(ForksController, self).__before__()
44 52
53 def __load_defaults(self):
54 c.repo_groups = RepoGroup.groups_choices()
55 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
56
57 def __load_data(self, repo_name=None):
58 """
59 Load defaults settings for edit, and update
60
61 :param repo_name:
62 """
63 self.__load_defaults()
64
65 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
66 repo = db_repo.scm_instance
67
68 if c.repo_info is None:
69 h.flash(_('%s repository is not mapped to db perhaps'
70 ' it was created or renamed from the filesystem'
71 ' please run the application again'
72 ' in order to rescan repositories') % repo_name,
73 category='error')
74
75 return redirect(url('repos'))
76
77 c.default_user_id = User.get_by_username('default').user_id
78 c.in_public_journal = UserFollowing.query()\
79 .filter(UserFollowing.user_id == c.default_user_id)\
80 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
81
82 if c.repo_info.stats:
83 last_rev = c.repo_info.stats.stat_on_revision+1
84 else:
85 last_rev = 0
86 c.stats_revision = last_rev
87
88 c.repo_last_rev = repo.count() if repo.revisions else 0
89
90 if last_rev == 0 or c.repo_last_rev == 0:
91 c.stats_percentage = 0
92 else:
93 c.stats_percentage = '%.2f' % ((float((last_rev)) /
94 c.repo_last_rev) * 100)
95
96 defaults = RepoModel()._get_defaults(repo_name)
97 # add prefix to fork
98 defaults['repo_name'] = 'fork-' + defaults['repo_name']
99 return defaults
100
101 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
102 'repository.admin')
45 103 def forks(self, repo_name):
46 104 p = int(request.params.get('page', 1))
47 105 repo_id = c.rhodecode_db_repo.repo_id
48 106 d = Repository.get_repo_forks(repo_id)
49 107 c.forks_pager = Page(d, page=p, items_per_page=20)
50 108
51 109 c.forks_data = render('/forks/forks_data.html')
52 110
53 111 if request.environ.get('HTTP_X_PARTIAL_XHR'):
54 112 return c.forks_data
55 113
56 114 return render('/forks/forks.html')
115
116 @NotAnonymous()
117 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 'repository.admin')
119 def fork(self, repo_name):
120 c.repo_info = Repository.get_by_repo_name(repo_name)
121 if not c.repo_info:
122 h.flash(_('%s repository is not mapped to db perhaps'
123 ' it was created or renamed from the file system'
124 ' please run the application again'
125 ' in order to rescan repositories') % repo_name,
126 category='error')
127
128 return redirect(url('home'))
129
130 defaults = self.__load_data(repo_name)
131
132 return htmlfill.render(
133 render('forks/fork.html'),
134 defaults=defaults,
135 encoding="UTF-8",
136 force_defaults=False
137 )
138
139
140 @NotAnonymous()
141 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
142 'repository.admin')
143 def fork_create(self, repo_name):
144 self.__load_defaults()
145 c.repo_info = Repository.get_by_repo_name(repo_name)
146 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
147 repo_groups=c.repo_groups_choices,)()
148 form_result = {}
149 try:
150 form_result = _form.to_python(dict(request.POST))
151 # add org_path of repo so we can do a clone from it later
152 form_result['org_path'] = c.repo_info.repo_name
153
154 # create fork is done sometimes async on celery, db transaction
155 # management is handled there.
156 RepoModel().create_fork(form_result, self.rhodecode_user)
157 h.flash(_('forked %s repository as %s') \
158 % (repo_name, form_result['repo_name']),
159 category='success')
160 except formencode.Invalid, errors:
161 c.new_repo = errors.value['repo_name']
162
163 return htmlfill.render(
164 render('forks/fork.html'),
165 defaults=errors.value,
166 errors=errors.error_dict or {},
167 prefix_error=False,
168 encoding="UTF-8")
169 except Exception:
170 log.error(traceback.format_exc())
171 h.flash(_('An error occurred during repository forking %s') %
172 repo_name, category='error')
173
174 return redirect(url('home'))
@@ -1,60 +1,65 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.home
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Home controller for Rhodecode
7 7
8 8 :created_on: Feb 18, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 from operator import itemgetter
28 27
29 28 from pylons import tmpl_context as c, request
30 29 from paste.httpexceptions import HTTPBadRequest
31 30
32 31 from rhodecode.lib.auth import LoginRequired
33 32 from rhodecode.lib.base import BaseController, render
34 from rhodecode.model.db import Group, Repository
33 from rhodecode.model.db import Repository
35 34
36 35 log = logging.getLogger(__name__)
37 36
38 37
39 38 class HomeController(BaseController):
40 39
41 40 @LoginRequired()
42 41 def __before__(self):
43 42 super(HomeController, self).__before__()
44 43
45 44 def index(self):
46
47 45 c.repos_list = self.scm_model.get_repos()
48
49 c.groups = Group.query().filter(Group.group_parent_id == None).all()
46 c.groups = self.scm_model.get_repos_groups()
50 47
51 48 return render('/index.html')
52 49
53 50 def repo_switcher(self):
54 51 if request.is_xhr:
55 52 all_repos = Repository.query().order_by(Repository.repo_name).all()
56 53 c.repos_list = self.scm_model.get_repos(all_repos,
57 54 sort_key='name_sort')
58 55 return render('/repo_switcher_list.html')
59 56 else:
60 57 return HTTPBadRequest()
58
59 def branch_tag_switcher(self, repo_name):
60 if request.is_xhr:
61 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
62 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
63 return render('/switch_to_list.html')
64 else:
65 return HTTPBadRequest()
@@ -1,230 +1,242 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.journal
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Journal controller for pylons
7 7
8 8 :created_on: Nov 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 from itertools import groupby
26 27
27 28 from sqlalchemy import or_
28 from sqlalchemy.orm import joinedload, make_transient
29 from sqlalchemy.orm import joinedload
29 30 from webhelpers.paginate import Page
30 from itertools import groupby
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
31 32
32 33 from paste.httpexceptions import HTTPBadRequest
33 34 from pylons import request, tmpl_context as c, response, url
34 35 from pylons.i18n.translation import _
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36 36
37 37 import rhodecode.lib.helpers as h
38 38 from rhodecode.lib.auth import LoginRequired, NotAnonymous
39 39 from rhodecode.lib.base import BaseController, render
40 from rhodecode.model.db import UserLog, UserFollowing
40 from rhodecode.model.db import UserLog, UserFollowing, Repository, User
41 from rhodecode.model.meta import Session
42 from sqlalchemy.sql.expression import func
43 from rhodecode.model.scm import ScmModel
41 44
42 45 log = logging.getLogger(__name__)
43 46
44 47
45 48 class JournalController(BaseController):
46 49
47 50 def __before__(self):
48 51 super(JournalController, self).__before__()
49 52 self.rhodecode_user = self.rhodecode_user
50 53 self.title = _('%s public journal %s feed') % (c.rhodecode_name, '%s')
51 54 self.language = 'en-us'
52 55 self.ttl = "5"
53 56 self.feed_nr = 20
54 57
55 58 @LoginRequired()
56 59 @NotAnonymous()
57 60 def index(self):
58 61 # Return a rendered template
59 62 p = int(request.params.get('page', 1))
60 63
64 c.user = User.get(self.rhodecode_user.user_id)
65 all_repos = self.sa.query(Repository)\
66 .filter(Repository.user_id == c.user.user_id)\
67 .order_by(func.lower(Repository.repo_name)).all()
68
69 c.user_repos = ScmModel().get_repos(all_repos)
70
61 71 c.following = self.sa.query(UserFollowing)\
62 72 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
63 73 .options(joinedload(UserFollowing.follows_repository))\
64 74 .all()
65 75
66 76 journal = self._get_journal_data(c.following)
67 77
68 78 c.journal_pager = Page(journal, page=p, items_per_page=20)
69 79
70 80 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
71 81
72 82 c.journal_data = render('journal/journal_data.html')
73 83 if request.environ.get('HTTP_X_PARTIAL_XHR'):
74 84 return c.journal_data
75 85 return render('journal/journal.html')
76 86
77 87 def _get_daily_aggregate(self, journal):
78 88 groups = []
79 89 for k, g in groupby(journal, lambda x: x.action_as_day):
80 90 user_group = []
81 91 for k2, g2 in groupby(list(g), lambda x: x.user.email):
82 92 l = list(g2)
83 93 user_group.append((l[0].user, l))
84 94
85 95 groups.append((k, user_group,))
86 96
87 97 return groups
88 98
89 99 def _get_journal_data(self, following_repos):
90 100 repo_ids = [x.follows_repository.repo_id for x in following_repos
91 101 if x.follows_repository is not None]
92 102 user_ids = [x.follows_user.user_id for x in following_repos
93 103 if x.follows_user is not None]
94 104
95 105 filtering_criterion = None
96 106
97 107 if repo_ids and user_ids:
98 108 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
99 109 UserLog.user_id.in_(user_ids))
100 110 if repo_ids and not user_ids:
101 111 filtering_criterion = UserLog.repository_id.in_(repo_ids)
102 112 if not repo_ids and user_ids:
103 113 filtering_criterion = UserLog.user_id.in_(user_ids)
104 114 if filtering_criterion is not None:
105 115 journal = self.sa.query(UserLog)\
106 116 .options(joinedload(UserLog.user))\
107 117 .options(joinedload(UserLog.repository))\
108 118 .filter(filtering_criterion)\
109 119 .order_by(UserLog.action_date.desc())
110 120 else:
111 121 journal = []
112 122
113 123 return journal
114 124
115 125 @LoginRequired()
116 126 @NotAnonymous()
117 127 def toggle_following(self):
118 128 cur_token = request.POST.get('auth_token')
119 129 token = h.get_token()
120 130 if cur_token == token:
121 131
122 132 user_id = request.POST.get('follows_user_id')
123 133 if user_id:
124 134 try:
125 135 self.scm_model.toggle_following_user(user_id,
126 136 self.rhodecode_user.user_id)
137 Session.commit()
127 138 return 'ok'
128 139 except:
129 140 raise HTTPBadRequest()
130 141
131 142 repo_id = request.POST.get('follows_repo_id')
132 143 if repo_id:
133 144 try:
134 145 self.scm_model.toggle_following_repo(repo_id,
135 146 self.rhodecode_user.user_id)
147 Session.commit()
136 148 return 'ok'
137 149 except:
138 150 raise HTTPBadRequest()
139 151
140 log.debug('token mismatch %s vs %s', cur_token, token)
152 log.debug('token mismatch %s vs %s' % (cur_token, token))
141 153 raise HTTPBadRequest()
142 154
143 155 @LoginRequired()
144 156 def public_journal(self):
145 157 # Return a rendered template
146 158 p = int(request.params.get('page', 1))
147 159
148 160 c.following = self.sa.query(UserFollowing)\
149 161 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
150 162 .options(joinedload(UserFollowing.follows_repository))\
151 163 .all()
152 164
153 165 journal = self._get_journal_data(c.following)
154 166
155 167 c.journal_pager = Page(journal, page=p, items_per_page=20)
156 168
157 169 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
158 170
159 171 c.journal_data = render('journal/journal_data.html')
160 172 if request.environ.get('HTTP_X_PARTIAL_XHR'):
161 173 return c.journal_data
162 174 return render('journal/public_journal.html')
163 175
164 176 @LoginRequired(api_access=True)
165 177 def public_journal_atom(self):
166 178 """
167 179 Produce an atom-1.0 feed via feedgenerator module
168 180 """
169 181 c.following = self.sa.query(UserFollowing)\
170 182 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
171 183 .options(joinedload(UserFollowing.follows_repository))\
172 184 .all()
173 185
174 186 journal = self._get_journal_data(c.following)
175 187
176 188 feed = Atom1Feed(title=self.title % 'atom',
177 189 link=url('public_journal_atom', qualified=True),
178 190 description=_('Public journal'),
179 191 language=self.language,
180 192 ttl=self.ttl)
181 193
182 194 for entry in journal[:self.feed_nr]:
183 195 #tmpl = h.action_parser(entry)[0]
184 196 action, action_extra = h.action_parser(entry, feed=True)
185 197 title = "%s - %s %s" % (entry.user.short_contact, action,
186 198 entry.repository.repo_name)
187 199 desc = action_extra()
188 200 feed.add_item(title=title,
189 201 pubdate=entry.action_date,
190 202 link=url('', qualified=True),
191 203 author_email=entry.user.email,
192 204 author_name=entry.user.full_contact,
193 205 description=desc)
194 206
195 207 response.content_type = feed.mime_type
196 208 return feed.writeString('utf-8')
197 209
198 210 @LoginRequired(api_access=True)
199 211 def public_journal_rss(self):
200 212 """
201 213 Produce an rss2 feed via feedgenerator module
202 214 """
203 215 c.following = self.sa.query(UserFollowing)\
204 216 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
205 217 .options(joinedload(UserFollowing.follows_repository))\
206 218 .all()
207 219
208 220 journal = self._get_journal_data(c.following)
209 221
210 222 feed = Rss201rev2Feed(title=self.title % 'rss',
211 223 link=url('public_journal_rss', qualified=True),
212 224 description=_('Public journal'),
213 225 language=self.language,
214 226 ttl=self.ttl)
215 227
216 228 for entry in journal[:self.feed_nr]:
217 229 #tmpl = h.action_parser(entry)[0]
218 230 action, action_extra = h.action_parser(entry, feed=True)
219 231 title = "%s - %s %s" % (entry.user.short_contact, action,
220 232 entry.repository.repo_name)
221 233 desc = action_extra()
222 234 feed.add_item(title=title,
223 235 pubdate=entry.action_date,
224 236 link=url('', qualified=True),
225 237 author_email=entry.user.email,
226 238 author_name=entry.user.full_contact,
227 239 description=desc)
228 240
229 241 response.content_type = feed.mime_type
230 242 return feed.writeString('utf-8')
@@ -1,166 +1,169 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.login
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Login controller for rhodeocode
7 7
8 8 :created_on: Apr 22, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import formencode
28 28
29 29 from formencode import htmlfill
30 30
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34
35 35 import rhodecode.lib.helpers as h
36 36 from rhodecode.lib.auth import AuthUser, HasPermissionAnyDecorator
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
40 40 from rhodecode.model.user import UserModel
41 from rhodecode.model.meta import Session
41 42
42 43
43 44 log = logging.getLogger(__name__)
44 45
45 46
46 47 class LoginController(BaseController):
47 48
48 49 def __before__(self):
49 50 super(LoginController, self).__before__()
50 51
51 52 def index(self):
52 #redirect if already logged in
53 # redirect if already logged in
53 54 c.came_from = request.GET.get('came_from', None)
54 55
55 56 if self.rhodecode_user.is_authenticated \
56 57 and self.rhodecode_user.username != 'default':
57 58
58 59 return redirect(url('home'))
59 60
60 61 if request.POST:
61 #import Login Form validator class
62 # import Login Form validator class
62 63 login_form = LoginForm()
63 64 try:
64 65 c.form_result = login_form.to_python(dict(request.POST))
65 #form checks for username/password, now we're authenticated
66 # form checks for username/password, now we're authenticated
66 67 username = c.form_result['username']
67 68 user = User.get_by_username(username, case_insensitive=True)
68 69 auth_user = AuthUser(user.user_id)
69 70 auth_user.set_authenticated()
70 session['rhodecode_user'] = auth_user
71 cs = auth_user.get_cookie_store()
72 session['rhodecode_user'] = cs
73 # If they want to be remembered, update the cookie
74 if c.form_result['remember'] is not False:
75 session.cookie_expires = False
76 session._set_cookie_values()
77 session._update_cookie_out()
71 78 session.save()
72 79
73 log.info('user %s is now authenticated and stored in session',
74 username)
80 log.info('user %s is now authenticated and stored in '
81 'session, session attrs %s' % (username, cs))
75 82 user.update_lastlogin()
83 Session.commit()
76 84
77 85 if c.came_from:
78 86 return redirect(c.came_from)
79 87 else:
80 88 return redirect(url('home'))
81 89
82 90 except formencode.Invalid, errors:
83 91 return htmlfill.render(
84 92 render('/login.html'),
85 93 defaults=errors.value,
86 94 errors=errors.error_dict or {},
87 95 prefix_error=False,
88 96 encoding="UTF-8")
89 97
90 98 return render('/login.html')
91 99
92 100 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
93 101 'hg.register.manual_activate')
94 102 def register(self):
95 user_model = UserModel()
96 103 c.auto_active = False
97 104 for perm in User.get_by_username('default').user_perms:
98 105 if perm.permission.permission_name == 'hg.register.auto_activate':
99 106 c.auto_active = True
100 107 break
101 108
102 109 if request.POST:
103 110
104 111 register_form = RegisterForm()()
105 112 try:
106 113 form_result = register_form.to_python(dict(request.POST))
107 114 form_result['active'] = c.auto_active
108 user_model.create_registration(form_result)
115 UserModel().create_registration(form_result)
109 116 h.flash(_('You have successfully registered into rhodecode'),
110 117 category='success')
118 Session.commit()
111 119 return redirect(url('login_home'))
112 120
113 121 except formencode.Invalid, errors:
114 122 return htmlfill.render(
115 123 render('/register.html'),
116 124 defaults=errors.value,
117 125 errors=errors.error_dict or {},
118 126 prefix_error=False,
119 127 encoding="UTF-8")
120 128
121 129 return render('/register.html')
122 130
123 131 def password_reset(self):
124 user_model = UserModel()
125 132 if request.POST:
126
127 133 password_reset_form = PasswordResetForm()()
128 134 try:
129 135 form_result = password_reset_form.to_python(dict(request.POST))
130 user_model.reset_password_link(form_result)
136 UserModel().reset_password_link(form_result)
131 137 h.flash(_('Your password reset link was sent'),
132 138 category='success')
133 139 return redirect(url('login_home'))
134 140
135 141 except formencode.Invalid, errors:
136 142 return htmlfill.render(
137 143 render('/password_reset.html'),
138 144 defaults=errors.value,
139 145 errors=errors.error_dict or {},
140 146 prefix_error=False,
141 147 encoding="UTF-8")
142 148
143 149 return render('/password_reset.html')
144 150
145 151 def password_reset_confirmation(self):
146
147 152 if request.GET and request.GET.get('key'):
148 153 try:
149 user_model = UserModel()
150 154 user = User.get_by_api_key(request.GET.get('key'))
151 155 data = dict(email=user.email)
152 user_model.reset_password(data)
156 UserModel().reset_password(data)
153 157 h.flash(_('Your password reset was successful, '
154 158 'new password has been sent to your email'),
155 159 category='success')
156 160 except Exception, e:
157 161 log.error(e)
158 162 return redirect(url('reset_password'))
159 163
160 164 return redirect(url('login_home'))
161 165
162 166 def logout(self):
163 del session['rhodecode_user']
164 session.save()
165 log.info('Logging out and setting user as Empty')
167 session.delete()
168 log.info('Logging out and deleting session for user')
166 169 redirect(url('home'))
@@ -1,121 +1,126 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.search
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Search controller for rhodecode
7 7
8 8 :created_on: Aug 7, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27
28 28 from pylons.i18n.translation import _
29 from pylons import request, config, session, tmpl_context as c
29 from pylons import request, config, tmpl_context as c
30 30
31 31 from rhodecode.lib.auth import LoginRequired
32 32 from rhodecode.lib.base import BaseController, render
33 33 from rhodecode.lib.indexers import SCHEMA, IDX_NAME, ResultWrapper
34 34
35 35 from webhelpers.paginate import Page
36 36 from webhelpers.util import update_params
37 37
38 38 from whoosh.index import open_dir, EmptyIndexError
39 39 from whoosh.qparser import QueryParser, QueryParserError
40 40 from whoosh.query import Phrase, Wildcard, Term, Prefix
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class SearchController(BaseController):
46 46
47 47 @LoginRequired()
48 48 def __before__(self):
49 49 super(SearchController, self).__before__()
50 50
51 51 def index(self, search_repo=None):
52 52 c.repo_name = search_repo
53 53 c.formated_results = []
54 54 c.runtime = ''
55 55 c.cur_query = request.GET.get('q', None)
56 56 c.cur_type = request.GET.get('type', 'source')
57 57 c.cur_search = search_type = {'content': 'content',
58 58 'commit': 'content',
59 59 'path': 'path',
60 60 'repository': 'repository'}\
61 61 .get(c.cur_type, 'content')
62 62
63 63 if c.cur_query:
64 64 cur_query = c.cur_query.lower()
65 65
66 66 if c.cur_query:
67 67 p = int(request.params.get('page', 1))
68 68 highlight_items = set()
69 69 try:
70 70 idx = open_dir(config['app_conf']['index_dir'],
71 71 indexname=IDX_NAME)
72 72 searcher = idx.searcher()
73 73
74 74 qp = QueryParser(search_type, schema=SCHEMA)
75 75 if c.repo_name:
76 76 cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
77 77 try:
78 78 query = qp.parse(unicode(cur_query))
79
79 # extract words for highlight
80 80 if isinstance(query, Phrase):
81 81 highlight_items.update(query.words)
82 82 elif isinstance(query, Prefix):
83 83 highlight_items.add(query.text)
84 84 else:
85 85 for i in query.all_terms():
86 86 if i[0] == 'content':
87 87 highlight_items.add(i[1])
88 88
89 89 matcher = query.matcher(searcher)
90 90
91 91 log.debug(query)
92 92 log.debug(highlight_items)
93 93 results = searcher.search(query)
94 94 res_ln = len(results)
95 c.runtime = '%s results (%.3f seconds)' \
96 % (res_ln, results.runtime)
95 c.runtime = '%s results (%.3f seconds)' % (
96 res_ln, results.runtime
97 )
97 98
98 99 def url_generator(**kw):
99 100 return update_params("?q=%s&type=%s" \
100 101 % (c.cur_query, c.cur_search), **kw)
101 102
102 103 c.formated_results = Page(
103 ResultWrapper(search_type, searcher, matcher,
104 highlight_items),
105 page=p, item_count=res_ln,
106 items_per_page=10, url=url_generator)
104 ResultWrapper(search_type, searcher, matcher,
105 highlight_items),
106 page=p,
107 item_count=res_ln,
108 items_per_page=10,
109 url=url_generator
110 )
107 111
108 112 except QueryParserError:
109 113 c.runtime = _('Invalid search query. Try quoting it.')
110 114 searcher.close()
111 115 except (EmptyIndexError, IOError):
112 116 log.error(traceback.format_exc())
113 117 log.error('Empty Index data')
114 118 c.runtime = _('There is no index to search in. '
115 119 'Please run whoosh indexer')
116 120 except (Exception):
117 121 log.error(traceback.format_exc())
118 122 c.runtime = _('An error occurred during this search operation')
119 123
124
120 125 # Return a rendered template
121 126 return render('/search/search.html')
@@ -1,208 +1,158 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Settings controller for rhodecode
7 7
8 8 :created_on: Jun 30, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31
32 32 from pylons import tmpl_context as c, request, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 import rhodecode.lib.helpers as h
37 37
38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator, \
39 HasRepoPermissionAnyDecorator, NotAnonymous
38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
40 39 from rhodecode.lib.base import BaseRepoController, render
41 40 from rhodecode.lib.utils import invalidate_cache, action_logger
42 41
43 from rhodecode.model.forms import RepoSettingsForm, RepoForkForm
42 from rhodecode.model.forms import RepoSettingsForm
44 43 from rhodecode.model.repo import RepoModel
45 from rhodecode.model.db import Group
44 from rhodecode.model.db import RepoGroup
45 from rhodecode.model.meta import Session
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class SettingsController(BaseRepoController):
51 51
52 52 @LoginRequired()
53 53 def __before__(self):
54 54 super(SettingsController, self).__before__()
55
55
56 56 def __load_defaults(self):
57 c.repo_groups = Group.groups_choices()
57 c.repo_groups = RepoGroup.groups_choices()
58 58 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
59
59
60 60 repo_model = RepoModel()
61 61 c.users_array = repo_model.get_users_js()
62 62 c.users_groups_array = repo_model.get_users_groups_js()
63
63
64 64 @HasRepoPermissionAllDecorator('repository.admin')
65 65 def index(self, repo_name):
66 66 repo_model = RepoModel()
67 67 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
68 68 if not repo:
69 69 h.flash(_('%s repository is not mapped to db perhaps'
70 70 ' it was created or renamed from the file system'
71 71 ' please run the application again'
72 72 ' in order to rescan repositories') % repo_name,
73 73 category='error')
74 74
75 75 return redirect(url('home'))
76 76
77 77 self.__load_defaults()
78 78
79 79 defaults = RepoModel()._get_defaults(repo_name)
80 80
81 81 return htmlfill.render(
82 82 render('settings/repo_settings.html'),
83 83 defaults=defaults,
84 84 encoding="UTF-8",
85 85 force_defaults=False
86 86 )
87 87
88 88 @HasRepoPermissionAllDecorator('repository.admin')
89 89 def update(self, repo_name):
90 90 repo_model = RepoModel()
91 91 changed_name = repo_name
92
92
93 93 self.__load_defaults()
94
94
95 95 _form = RepoSettingsForm(edit=True,
96 96 old_data={'repo_name': repo_name},
97 97 repo_groups=c.repo_groups_choices)()
98 98 try:
99 99 form_result = _form.to_python(dict(request.POST))
100
100
101 101 repo_model.update(repo_name, form_result)
102 102 invalidate_cache('get_repo_cached_%s' % repo_name)
103 103 h.flash(_('Repository %s updated successfully' % repo_name),
104 104 category='success')
105 105 changed_name = form_result['repo_name_full']
106 106 action_logger(self.rhodecode_user, 'user_updated_repo',
107 107 changed_name, '', self.sa)
108 Session.commit()
108 109 except formencode.Invalid, errors:
109 110 c.repo_info = repo_model.get_by_repo_name(repo_name)
110 111 c.users_array = repo_model.get_users_js()
111 112 errors.value.update({'user': c.repo_info.user.username})
112 113 return htmlfill.render(
113 114 render('settings/repo_settings.html'),
114 115 defaults=errors.value,
115 116 errors=errors.error_dict or {},
116 117 prefix_error=False,
117 118 encoding="UTF-8")
118 119 except Exception:
119 120 log.error(traceback.format_exc())
120 121 h.flash(_('error occurred during update of repository %s') \
121 122 % repo_name, category='error')
122 123
123 124 return redirect(url('repo_settings_home', repo_name=changed_name))
124 125
125 126 @HasRepoPermissionAllDecorator('repository.admin')
126 127 def delete(self, repo_name):
127 128 """DELETE /repos/repo_name: Delete an existing item"""
128 129 # Forms posted to this method should contain a hidden field:
129 130 # <input type="hidden" name="_method" value="DELETE" />
130 131 # Or using helpers:
131 132 # h.form(url('repo_settings_delete', repo_name=ID),
132 133 # method='delete')
133 134 # url('repo_settings_delete', repo_name=ID)
134 135
135 136 repo_model = RepoModel()
136 137 repo = repo_model.get_by_repo_name(repo_name)
137 138 if not repo:
138 139 h.flash(_('%s repository is not mapped to db perhaps'
139 140 ' it was moved or renamed from the filesystem'
140 141 ' please run the application again'
141 142 ' in order to rescan repositories') % repo_name,
142 143 category='error')
143 144
144 145 return redirect(url('home'))
145 146 try:
146 147 action_logger(self.rhodecode_user, 'user_deleted_repo',
147 148 repo_name, '', self.sa)
148 149 repo_model.delete(repo)
149 150 invalidate_cache('get_repo_cached_%s' % repo_name)
150 151 h.flash(_('deleted repository %s') % repo_name, category='success')
152 Session.commit()
151 153 except Exception:
152 154 log.error(traceback.format_exc())
153 155 h.flash(_('An error occurred during deletion of %s') % repo_name,
154 156 category='error')
155 157
156 158 return redirect(url('home'))
157
158 @NotAnonymous()
159 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
160 'repository.admin')
161 def fork(self, repo_name):
162 repo_model = RepoModel()
163 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
164 if not repo:
165 h.flash(_('%s repository is not mapped to db perhaps'
166 ' it was created or renamed from the file system'
167 ' please run the application again'
168 ' in order to rescan repositories') % repo_name,
169 category='error')
170
171 return redirect(url('home'))
172
173 return render('settings/repo_fork.html')
174
175 @NotAnonymous()
176 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
177 'repository.admin')
178 def fork_create(self, repo_name):
179 repo_model = RepoModel()
180 c.repo_info = repo_model.get_by_repo_name(repo_name)
181 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type})()
182 form_result = {}
183 try:
184 form_result = _form.to_python(dict(request.POST))
185 form_result.update({'repo_name': repo_name})
186 repo_model.create_fork(form_result, self.rhodecode_user)
187 h.flash(_('forked %s repository as %s') \
188 % (repo_name, form_result['fork_name']),
189 category='success')
190 action_logger(self.rhodecode_user,
191 'user_forked_repo:%s' % form_result['fork_name'],
192 repo_name, '', self.sa)
193 except formencode.Invalid, errors:
194 c.new_repo = errors.value['fork_name']
195 r = render('settings/repo_fork.html')
196
197 return htmlfill.render(
198 r,
199 defaults=errors.value,
200 errors=errors.error_dict or {},
201 prefix_error=False,
202 encoding="UTF-8")
203 except Exception:
204 log.error(traceback.format_exc())
205 h.flash(_('An error occurred during repository forking %s') %
206 repo_name, category='error')
207
208 return redirect(url('home'))
@@ -1,59 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.shortlog
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Shortlog controller for rhodecode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import tmpl_context as c, request, url
29 29
30 30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
31 31 from rhodecode.lib.base import BaseRepoController, render
32 32 from rhodecode.lib.helpers import RepoPage
33 from pylons.controllers.util import redirect
33 34
34 35 log = logging.getLogger(__name__)
35 36
36 37
37 38 class ShortlogController(BaseRepoController):
38 39
39 40 @LoginRequired()
40 41 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 42 'repository.admin')
42 43 def __before__(self):
43 44 super(ShortlogController, self).__before__()
44 45
45 46 def index(self, repo_name):
46 47 p = int(request.params.get('page', 1))
47 48 size = int(request.params.get('size', 20))
48 49
49 50 def url_generator(**kw):
50 51 return url('shortlog_home', repo_name=repo_name, size=size, **kw)
51 52
52 53 c.repo_changesets = RepoPage(c.rhodecode_repo, page=p,
53 items_per_page=size,
54 url=url_generator)
54 items_per_page=size, url=url_generator)
55
56 if not c.repo_changesets:
57 return redirect(url('summary_home', repo_name=repo_name))
58
55 59 c.shortlog_data = render('shortlog/shortlog_data.html')
56 60 if request.environ.get('HTTP_X_PARTIAL_XHR'):
57 61 return c.shortlog_data
58 62 r = render('shortlog/shortlog.html')
59 63 return r
@@ -1,183 +1,233 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.summary
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Summary controller for Rhodecode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 import traceback
26 27 import calendar
27 28 import logging
28 29 from time import mktime
29 from datetime import datetime, timedelta, date
30 from datetime import timedelta, date
31 from itertools import product
32 from urlparse import urlparse
30 33
31 from vcs.exceptions import ChangesetError
34 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
35 NodeDoesNotExistError
32 36
33 from pylons import tmpl_context as c, request, url
37 from pylons import tmpl_context as c, request, url, config
34 38 from pylons.i18n.translation import _
35 39
36 from rhodecode.model.db import Statistics, Repository
37 from rhodecode.model.repo import RepoModel
40 from beaker.cache import cache_region, region_invalidate
38 41
42 from rhodecode.model.db import Statistics, CacheInvalidation
43 from rhodecode.lib import ALL_READMES, ALL_EXTS
39 44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
40 45 from rhodecode.lib.base import BaseRepoController, render
41 46 from rhodecode.lib.utils import EmptyChangeset
42
47 from rhodecode.lib.markup_renderer import MarkupRenderer
43 48 from rhodecode.lib.celerylib import run_task
44 49 from rhodecode.lib.celerylib.tasks import get_commits_stats, \
45 50 LANGUAGES_EXTENSIONS_MAP
46 51 from rhodecode.lib.helpers import RepoPage
47 52 from rhodecode.lib.compat import json, OrderedDict
48 53
49 54 log = logging.getLogger(__name__)
50 55
56 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
57 sorted(list(product(ALL_READMES, ALL_EXTS)),
58 key=lambda y:y[0][1] + y[1][1])]
59
51 60
52 61 class SummaryController(BaseRepoController):
53 62
54 63 @LoginRequired()
55 64 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
56 65 'repository.admin')
57 66 def __before__(self):
58 67 super(SummaryController, self).__before__()
59 68
60 69 def index(self, repo_name):
61
62 e = request.environ
63 70 c.dbrepo = dbrepo = c.rhodecode_db_repo
64
65 71 c.following = self.scm_model.is_following_repo(repo_name,
66 72 self.rhodecode_user.user_id)
67 73
68 74 def url_generator(**kw):
69 75 return url('shortlog_home', repo_name=repo_name, size=10, **kw)
70 76
71 77 c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
72 78 items_per_page=10, url=url_generator)
73 79
74 80 if self.rhodecode_user.username == 'default':
75 #for default(anonymous) user we don't need to pass credentials
81 # for default(anonymous) user we don't need to pass credentials
76 82 username = ''
77 83 password = ''
78 84 else:
79 85 username = str(self.rhodecode_user.username)
80 86 password = '@'
81 87
82 if e.get('wsgi.url_scheme') == 'https':
83 split_s = 'https://'
84 else:
85 split_s = 'http://'
88 parsed_url = urlparse(url.current(qualified=True))
89
90 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
91
92 uri_tmpl = config.get('clone_uri', default_clone_uri)
93 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
86 94
87 qualified_uri = [split_s] + [url.current(qualified=True)\
88 .split(split_s)[-1]]
89 uri = u'%(proto)s%(user)s%(pass)s%(rest)s' \
90 % {'user': username,
91 'pass': password,
92 'proto': qualified_uri[0],
93 'rest': qualified_uri[1]}
95 uri_dict = {
96 'user': username,
97 'pass': password,
98 'scheme': parsed_url.scheme,
99 'netloc': parsed_url.netloc,
100 'path': parsed_url.path
101 }
102 uri = uri_tmpl % uri_dict
103 # generate another clone url by id
104 uri_dict.update({'path': '/_%s' % c.dbrepo.repo_id})
105 uri_id = uri_tmpl % uri_dict
106
94 107 c.clone_repo_url = uri
108 c.clone_repo_url_id = uri_id
95 109 c.repo_tags = OrderedDict()
96 110 for name, hash in c.rhodecode_repo.tags.items()[:10]:
97 111 try:
98 112 c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash)
99 113 except ChangesetError:
100 114 c.repo_tags[name] = EmptyChangeset(hash)
101 115
102 116 c.repo_branches = OrderedDict()
103 117 for name, hash in c.rhodecode_repo.branches.items()[:10]:
104 118 try:
105 119 c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash)
106 120 except ChangesetError:
107 121 c.repo_branches[name] = EmptyChangeset(hash)
108 122
109 123 td = date.today() + timedelta(days=1)
110 124 td_1m = td - timedelta(days=calendar.mdays[td.month])
111 125 td_1y = td - timedelta(days=365)
112 126
113 127 ts_min_m = mktime(td_1m.timetuple())
114 128 ts_min_y = mktime(td_1y.timetuple())
115 129 ts_max_y = mktime(td.timetuple())
116 130
117 131 if dbrepo.enable_statistics:
118 132 c.show_stats = True
119 133 c.no_data_msg = _('No data loaded yet')
120 134 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
121 135 else:
122 136 c.show_stats = False
123 137 c.no_data_msg = _('Statistics are disabled for this repository')
124 138 c.ts_min = ts_min_m
125 139 c.ts_max = ts_max_y
126 140
127 141 stats = self.sa.query(Statistics)\
128 142 .filter(Statistics.repository == dbrepo)\
129 143 .scalar()
130 144
131 145 c.stats_percentage = 0
132 146
133 147 if stats and stats.languages:
134 148 c.no_data = False is dbrepo.enable_statistics
135 149 lang_stats_d = json.loads(stats.languages)
136 150 c.commit_data = stats.commit_activity
137 151 c.overview_data = stats.commit_activity_combined
138 152
139 153 lang_stats = ((x, {"count": y,
140 154 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
141 155 for x, y in lang_stats_d.items())
142 156
143 157 c.trending_languages = json.dumps(
144 158 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
145 159 )
146 160 last_rev = stats.stat_on_revision + 1
147 161 c.repo_last_rev = c.rhodecode_repo.count()\
148 162 if c.rhodecode_repo.revisions else 0
149 163 if last_rev == 0 or c.repo_last_rev == 0:
150 164 pass
151 165 else:
152 166 c.stats_percentage = '%.2f' % ((float((last_rev)) /
153 167 c.repo_last_rev) * 100)
154 168 else:
155 169 c.commit_data = json.dumps({})
156 170 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
157 171 c.trending_languages = json.dumps({})
158 172 c.no_data = True
159 173
160 174 c.enable_downloads = dbrepo.enable_downloads
161 175 if c.enable_downloads:
162 176 c.download_options = self._get_download_links(c.rhodecode_repo)
163 177
178 c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_repo)
164 179 return render('summary/summary.html')
165 180
181 def __get_readme_data(self, repo):
182
183 @cache_region('long_term')
184 def _get_readme_from_cache(key):
185 readme_data = None
186 readme_file = None
187 log.debug('Fetching readme file')
188 try:
189 cs = repo.get_changeset('tip')
190 renderer = MarkupRenderer()
191 for f in README_FILES:
192 try:
193 readme = cs.get_node(f)
194 readme_file = f
195 readme_data = renderer.render(readme.content, f)
196 log.debug('Found readme %s' % readme_file)
197 break
198 except NodeDoesNotExistError:
199 continue
200 except ChangesetError:
201 pass
202 except EmptyRepositoryError:
203 pass
204 except Exception:
205 log.error(traceback.format_exc())
206
207 return readme_data, readme_file
208
209 key = repo.name + '_README'
210 inv = CacheInvalidation.invalidate(key)
211 if inv is not None:
212 region_invalidate(_get_readme_from_cache, None, key)
213 CacheInvalidation.set_valid(inv.cache_key)
214 return _get_readme_from_cache(key)
215
166 216 def _get_download_links(self, repo):
167 217
168 218 download_l = []
169 219
170 220 branches_group = ([], _("Branches"))
171 221 tags_group = ([], _("Tags"))
172 222
173 223 for name, chs in c.rhodecode_repo.branches.items():
174 224 #chs = chs.split(':')[-1]
175 225 branches_group[0].append((chs, name),)
176 226 download_l.append(branches_group)
177 227
178 228 for name, chs in c.rhodecode_repo.tags.items():
179 229 #chs = chs.split(':')[-1]
180 230 tags_group[0].append((chs, name),)
181 231 download_l.append(tags_group)
182 232
183 233 return download_l
@@ -1,53 +1,53 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.tags
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Tags controller for rhodecode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26
27 27 from pylons import tmpl_context as c
28 28
29 29 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
30 30 from rhodecode.lib.base import BaseRepoController, render
31 31 from rhodecode.lib.compat import OrderedDict
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class TagsController(BaseRepoController):
37 37
38 38 @LoginRequired()
39 39 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
40 40 'repository.admin')
41 41 def __before__(self):
42 42 super(TagsController, self).__before__()
43 43
44 44 def index(self):
45 45 c.repo_tags = OrderedDict()
46 46
47 47 tags = [(name, c.rhodecode_repo.get_changeset(hash_)) for \
48 48 name, hash_ in c.rhodecode_repo.tags.items()]
49 49 ordered_tags = sorted(tags, key=lambda x: x[1].date, reverse=True)
50 50 for name, cs_tag in ordered_tags:
51 51 c.repo_tags[name] = cs_tag
52 52
53 53 return render('tags/tags.html')
@@ -1,410 +1,467 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 import re
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29
27 30
28 31 def __get_lem():
29 32 from pygments import lexers
30 33 from string import lower
31 34 from collections import defaultdict
32 35
33 36 d = defaultdict(lambda: [])
34 37
35 38 def __clean(s):
36 39 s = s.lstrip('*')
37 40 s = s.lstrip('.')
38 41
39 42 if s.find('[') != -1:
40 43 exts = []
41 44 start, stop = s.find('['), s.find(']')
42 45
43 46 for suffix in s[start + 1:stop]:
44 47 exts.append(s[:s.find('[')] + suffix)
45 48 return map(lower, exts)
46 49 else:
47 50 return map(lower, [s])
48 51
49 52 for lx, t in sorted(lexers.LEXERS.items()):
50 53 m = map(__clean, t[-2])
51 54 if m:
52 55 m = reduce(lambda x, y: x + y, m)
53 56 for ext in m:
54 57 desc = lx.replace('Lexer', '')
55 58 d[ext].append(desc)
56 59
57 60 return dict(d)
58 61
59 62 # language map is also used by whoosh indexer, which for those specified
60 63 # extensions will index it's content
61 64 LANGUAGES_EXTENSIONS_MAP = __get_lem()
62 65
63 66 # Additional mappings that are not present in the pygments lexers
64 67 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
65 68 ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}
66 69
67 70 LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
68 71
72 # list of readme files to search in file tree and display in summary
73 # attached weights defines the search order lower is first
74 ALL_READMES = [
75 ('readme', 0), ('README', 0), ('Readme', 0),
76 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
77 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
78 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
79 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
80 ]
81
82 # extension together with weights to search lower is first
83 RST_EXTS = [
84 ('', 0), ('.rst', 1), ('.rest', 1),
85 ('.RST', 2), ('.REST', 2),
86 ('.txt', 3), ('.TXT', 3)
87 ]
88
89 MARKDOWN_EXTS = [
90 ('.md', 1), ('.MD', 1),
91 ('.mkdn', 2), ('.MKDN', 2),
92 ('.mdown', 3), ('.MDOWN', 3),
93 ('.markdown', 4), ('.MARKDOWN', 4)
94 ]
95
96 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
97
98 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
99
69 100
70 101 def str2bool(_str):
71 102 """
72 103 returs True/False value from given string, it tries to translate the
73 104 string into boolean
74 105
75 106 :param _str: string value to translate into boolean
76 107 :rtype: boolean
77 108 :returns: boolean from given string
78 109 """
79 110 if _str is None:
80 111 return False
81 112 if _str in (True, False):
82 113 return _str
83 114 _str = str(_str).strip().lower()
84 115 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
85 116
86 117
87 118 def convert_line_endings(line, mode):
88 119 """
89 120 Converts a given line "line end" accordingly to given mode
90 121
91 122 Available modes are::
92 123 0 - Unix
93 124 1 - Mac
94 125 2 - DOS
95 126
96 127 :param line: given line to convert
97 128 :param mode: mode to convert to
98 129 :rtype: str
99 130 :return: converted line according to mode
100 131 """
101 132 from string import replace
102 133
103 134 if mode == 0:
104 135 line = replace(line, '\r\n', '\n')
105 136 line = replace(line, '\r', '\n')
106 137 elif mode == 1:
107 138 line = replace(line, '\r\n', '\r')
108 139 line = replace(line, '\n', '\r')
109 140 elif mode == 2:
110 import re
111 141 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
112 142 return line
113 143
114 144
115 145 def detect_mode(line, default):
116 146 """
117 147 Detects line break for given line, if line break couldn't be found
118 148 given default value is returned
119 149
120 150 :param line: str line
121 151 :param default: default
122 152 :rtype: int
123 153 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
124 154 """
125 155 if line.endswith('\r\n'):
126 156 return 2
127 157 elif line.endswith('\n'):
128 158 return 0
129 159 elif line.endswith('\r'):
130 160 return 1
131 161 else:
132 162 return default
133 163
134 164
135 165 def generate_api_key(username, salt=None):
136 166 """
137 167 Generates unique API key for given username, if salt is not given
138 168 it'll be generated from some random string
139 169
140 170 :param username: username as string
141 171 :param salt: salt to hash generate KEY
142 172 :rtype: str
143 173 :returns: sha1 hash from username+salt
144 174 """
145 175 from tempfile import _RandomNameSequence
146 176 import hashlib
147 177
148 178 if salt is None:
149 179 salt = _RandomNameSequence().next()
150 180
151 181 return hashlib.sha1(username + salt).hexdigest()
152 182
153 183
154 def safe_unicode(str_, from_encoding='utf8'):
184 def safe_unicode(str_, from_encoding=None):
155 185 """
156 186 safe unicode function. Does few trick to turn str_ into unicode
157 187
158 188 In case of UnicodeDecode error we try to return it with encoding detected
159 189 by chardet library if it fails fallback to unicode with errors replaced
160 190
161 191 :param str_: string to decode
162 192 :rtype: unicode
163 193 :returns: unicode object
164 194 """
165 195 if isinstance(str_, unicode):
166 196 return str_
167 197
198 if not from_encoding:
199 import rhodecode
200 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
201 from_encoding = DEFAULT_ENCODING
202
168 203 try:
169 204 return unicode(str_)
170 205 except UnicodeDecodeError:
171 206 pass
172 207
173 208 try:
174 209 return unicode(str_, from_encoding)
175 210 except UnicodeDecodeError:
176 211 pass
177 212
178 213 try:
179 214 import chardet
180 215 encoding = chardet.detect(str_)['encoding']
181 216 if encoding is None:
182 217 raise Exception()
183 218 return str_.decode(encoding)
184 219 except (ImportError, UnicodeDecodeError, Exception):
185 220 return unicode(str_, from_encoding, 'replace')
186 221
187 def safe_str(unicode_, to_encoding='utf8'):
222
223 def safe_str(unicode_, to_encoding=None):
188 224 """
189 225 safe str function. Does few trick to turn unicode_ into string
190
226
191 227 In case of UnicodeEncodeError we try to return it with encoding detected
192 228 by chardet library if it fails fallback to string with errors replaced
193 229
194 230 :param unicode_: unicode to encode
195 231 :rtype: str
196 232 :returns: str object
197 233 """
198 234
199 235 if not isinstance(unicode_, basestring):
200 236 return str(unicode_)
201 237
238 if not isinstance(unicode_, basestring):
239 return str(unicode_)
240
202 241 if isinstance(unicode_, str):
203 242 return unicode_
204 243
244 if not to_encoding:
245 import rhodecode
246 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
247 to_encoding = DEFAULT_ENCODING
248
205 249 try:
206 250 return unicode_.encode(to_encoding)
207 251 except UnicodeEncodeError:
208 252 pass
209 253
210 254 try:
211 255 import chardet
212 256 encoding = chardet.detect(unicode_)['encoding']
213 257 print encoding
214 258 if encoding is None:
215 259 raise UnicodeEncodeError()
216 260
217 261 return unicode_.encode(encoding)
218 262 except (ImportError, UnicodeEncodeError):
219 263 return unicode_.encode(to_encoding, 'replace')
220 264
221 265 return safe_str
222 266
223 267
224
225 268 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
226 269 """
227 270 Custom engine_from_config functions that makes sure we use NullPool for
228 file based sqlite databases. This prevents errors on sqlite. This only
271 file based sqlite databases. This prevents errors on sqlite. This only
229 272 applies to sqlalchemy versions < 0.7.0
230 273
231 274 """
232 275 import sqlalchemy
233 276 from sqlalchemy import engine_from_config as efc
234 277 import logging
235 278
236 279 if int(sqlalchemy.__version__.split('.')[1]) < 7:
237 280
238 281 # This solution should work for sqlalchemy < 0.7.0, and should use
239 282 # proxy=TimerProxy() for execution time profiling
240 283
241 284 from sqlalchemy.pool import NullPool
242 285 url = configuration[prefix + 'url']
243 286
244 287 if url.startswith('sqlite'):
245 288 kwargs.update({'poolclass': NullPool})
246 289 return efc(configuration, prefix, **kwargs)
247 290 else:
248 291 import time
249 292 from sqlalchemy import event
250 293 from sqlalchemy.engine import Engine
251 294
252 295 log = logging.getLogger('sqlalchemy.engine')
253 296 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
254 297 engine = efc(configuration, prefix, **kwargs)
255 298
256 299 def color_sql(sql):
257 300 COLOR_SEQ = "\033[1;%dm"
258 301 COLOR_SQL = YELLOW
259 302 normal = '\x1b[0m'
260 303 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
261 304
262 305 if configuration['debug']:
263 306 #attach events only for debug configuration
264 307
265 308 def before_cursor_execute(conn, cursor, statement,
266 309 parameters, context, executemany):
267 310 context._query_start_time = time.time()
268 311 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
269 312
270 313
271 314 def after_cursor_execute(conn, cursor, statement,
272 315 parameters, context, executemany):
273 316 total = time.time() - context._query_start_time
274 317 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
275 318
276 319 event.listen(engine, "before_cursor_execute",
277 320 before_cursor_execute)
278 321 event.listen(engine, "after_cursor_execute",
279 322 after_cursor_execute)
280 323
281 324 return engine
282 325
283 326
284 327 def age(curdate):
285 328 """
286 329 turns a datetime into an age string.
287
330
288 331 :param curdate: datetime object
289 332 :rtype: unicode
290 333 :returns: unicode words describing age
291 334 """
292 335
293 336 from datetime import datetime
294 337 from webhelpers.date import time_ago_in_words
295 338
296 _ = lambda s:s
339 _ = lambda s: s
297 340
298 341 if not curdate:
299 342 return ''
300 343
301 344 agescales = [(_(u"year"), 3600 * 24 * 365),
302 345 (_(u"month"), 3600 * 24 * 30),
303 346 (_(u"day"), 3600 * 24),
304 347 (_(u"hour"), 3600),
305 348 (_(u"minute"), 60),
306 349 (_(u"second"), 1), ]
307 350
308 351 age = datetime.now() - curdate
309 352 age_seconds = (age.days * agescales[2][1]) + age.seconds
310 353 pos = 1
311 354 for scale in agescales:
312 355 if scale[1] <= age_seconds:
313 if pos == 6:pos = 5
356 if pos == 6:
357 pos = 5
314 358 return '%s %s' % (time_ago_in_words(curdate,
315 359 agescales[pos][0]), _('ago'))
316 360 pos += 1
317 361
318 362 return _(u'just now')
319 363
320 364
321 365 def uri_filter(uri):
322 366 """
323 367 Removes user:password from given url string
324
368
325 369 :param uri:
326 370 :rtype: unicode
327 :returns: filtered list of strings
371 :returns: filtered list of strings
328 372 """
329 373 if not uri:
330 374 return ''
331 375
332 376 proto = ''
333 377
334 378 for pat in ('https://', 'http://'):
335 379 if uri.startswith(pat):
336 380 uri = uri[len(pat):]
337 381 proto = pat
338 382 break
339 383
340 384 # remove passwords and username
341 385 uri = uri[uri.find('@') + 1:]
342 386
343 387 # get the port
344 388 cred_pos = uri.find(':')
345 389 if cred_pos == -1:
346 390 host, port = uri, None
347 391 else:
348 392 host, port = uri[:cred_pos], uri[cred_pos + 1:]
349 393
350 394 return filter(None, [proto, host, port])
351 395
352 396
353 397 def credentials_filter(uri):
354 398 """
355 399 Returns a url with removed credentials
356
400
357 401 :param uri:
358 402 """
359 403
360 404 uri = uri_filter(uri)
361 405 #check if we have port
362 406 if len(uri) > 2 and uri[2]:
363 407 uri[2] = ':' + uri[2]
364 408
365 409 return ''.join(uri)
366 410
411
367 412 def get_changeset_safe(repo, rev):
368 413 """
369 Safe version of get_changeset if this changeset doesn't exists for a
414 Safe version of get_changeset if this changeset doesn't exists for a
370 415 repo it returns a Dummy one instead
371
416
372 417 :param repo:
373 418 :param rev:
374 419 """
375 from vcs.backends.base import BaseRepository
376 from vcs.exceptions import RepositoryError
420 from rhodecode.lib.vcs.backends.base import BaseRepository
421 from rhodecode.lib.vcs.exceptions import RepositoryError
377 422 if not isinstance(repo, BaseRepository):
378 423 raise Exception('You must pass an Repository '
379 424 'object as first argument got %s', type(repo))
380 425
381 426 try:
382 427 cs = repo.get_changeset(rev)
383 428 except RepositoryError:
384 429 from rhodecode.lib.utils import EmptyChangeset
385 430 cs = EmptyChangeset(requested_revision=rev)
386 431 return cs
387 432
388 433
389 434 def get_current_revision(quiet=False):
390 435 """
391 436 Returns tuple of (number, id) from repository containing this package
392 437 or None if repository could not be found.
393
438
394 439 :param quiet: prints error for fetching revision if True
395 440 """
396 441
397 442 try:
398 from vcs import get_repo
399 from vcs.utils.helpers import get_scm
443 from rhodecode.lib.vcs import get_repo
444 from rhodecode.lib.vcs.utils.helpers import get_scm
400 445 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
401 446 scm = get_scm(repopath)[0]
402 447 repo = get_repo(path=repopath, alias=scm)
403 448 tip = repo.get_changeset()
404 449 return (tip.revision, tip.short_id)
405 450 except Exception, err:
406 451 if not quiet:
407 452 print ("Cannot retrieve rhodecode's revision. Original error "
408 453 "was: %s" % err)
409 454 return None
410 455
456
457 def extract_mentioned_users(s):
458 """
459 Returns unique usernames from given string s that have @mention
460
461 :param s: string to get mentions
462 """
463 usrs = {}
464 for username in re.findall(r'(?:^@|\s@)(\w+)', s):
465 usrs[username] = username
466
467 return sorted(usrs.keys())
@@ -1,619 +1,806 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 11 :license: GPLv3, see COPYING for more details.
11 12 """
12 13 # This program is free software: you can redistribute it and/or modify
13 14 # it under the terms of the GNU General Public License as published by
14 15 # the Free Software Foundation, either version 3 of the License, or
15 16 # (at your option) any later version.
16 17 #
17 18 # This program is distributed in the hope that it will be useful,
18 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 21 # GNU General Public License for more details.
21 22 #
22 23 # You should have received a copy of the GNU General Public License
23 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 25
25 26 import random
26 27 import logging
27 28 import traceback
28 29 import hashlib
29 30
30 31 from tempfile import _RandomNameSequence
31 32 from decorator import decorator
32 33
33 from pylons import config, session, url, request
34 from pylons import config, url, request
34 35 from pylons.controllers.util import abort, redirect
35 36 from pylons.i18n.translation import _
36 37
37 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 from rhodecode.model.meta import Session
38 40
39 41 if __platform__ in PLATFORM_WIN:
40 42 from hashlib import sha256
41 43 if __platform__ in PLATFORM_OTHERS:
42 44 import bcrypt
43 45
44 46 from rhodecode.lib import str2bool, safe_unicode
45 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
46 from rhodecode.lib.utils import get_repo_slug
48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
47 49 from rhodecode.lib.auth_ldap import AuthLdap
48 50
49 51 from rhodecode.model import meta
50 52 from rhodecode.model.user import UserModel
51 from rhodecode.model.db import Permission, RhodeCodeSettings, User
53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
52 54
53 55 log = logging.getLogger(__name__)
54 56
55 57
56 58 class PasswordGenerator(object):
57 """This is a simple class for generating password from
58 different sets of characters
59 usage:
59 """
60 This is a simple class for generating password from different sets of
61 characters
62 usage::
63
60 64 passwd_gen = PasswordGenerator()
61 65 #print 8-letter password containing only big and small letters
62 66 of alphabet
63 67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
64 68 """
65 69 ALPHABETS_NUM = r'''1234567890'''
66 70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
67 71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
68 72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
69 73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
70 74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
71 75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
72 76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
73 77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
74 78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
75 79
76 80 def __init__(self, passwd=''):
77 81 self.passwd = passwd
78 82
79 83 def gen_password(self, length, type_=None):
80 84 if type_ is None:
81 85 type_ = self.ALPHABETS_FULL
82 86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
83 87 return self.passwd
84 88
85 89
86 90 class RhodeCodeCrypto(object):
87 91
88 92 @classmethod
89 93 def hash_string(cls, str_):
90 94 """
91 95 Cryptographic function used for password hashing based on pybcrypt
92 96 or pycrypto in windows
93 97
94 98 :param password: password to hash
95 99 """
96 100 if __platform__ in PLATFORM_WIN:
97 101 return sha256(str_).hexdigest()
98 102 elif __platform__ in PLATFORM_OTHERS:
99 103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
100 104 else:
101 105 raise Exception('Unknown or unsupported platform %s' \
102 106 % __platform__)
103 107
104 108 @classmethod
105 109 def hash_check(cls, password, hashed):
106 110 """
107 111 Checks matching password with it's hashed value, runs different
108 112 implementation based on platform it runs on
109 113
110 114 :param password: password
111 115 :param hashed: password in hashed form
112 116 """
113 117
114 118 if __platform__ in PLATFORM_WIN:
115 119 return sha256(password).hexdigest() == hashed
116 120 elif __platform__ in PLATFORM_OTHERS:
117 121 return bcrypt.hashpw(password, hashed) == hashed
118 122 else:
119 123 raise Exception('Unknown or unsupported platform %s' \
120 124 % __platform__)
121 125
122 126
123 127 def get_crypt_password(password):
124 128 return RhodeCodeCrypto.hash_string(password)
125 129
126 130
127 131 def check_password(password, hashed):
128 132 return RhodeCodeCrypto.hash_check(password, hashed)
129 133
130 134
131 def generate_api_key(username, salt=None):
135 def generate_api_key(str_, salt=None):
136 """
137 Generates API KEY from given string
138
139 :param str_:
140 :param salt:
141 """
142
132 143 if salt is None:
133 144 salt = _RandomNameSequence().next()
134 145
135 return hashlib.sha1(username + salt).hexdigest()
146 return hashlib.sha1(str_ + salt).hexdigest()
136 147
137 148
138 149 def authfunc(environ, username, password):
139 """Dummy authentication function used in Mercurial/Git/ and access control,
150 """
151 Dummy authentication wrapper function used in Mercurial and Git for
152 access control.
140 153
141 154 :param environ: needed only for using in Basic auth
142 155 """
143 156 return authenticate(username, password)
144 157
145 158
146 159 def authenticate(username, password):
147 """Authentication function used for access control,
160 """
161 Authentication function used for access control,
148 162 firstly checks for db authentication then if ldap is enabled for ldap
149 163 authentication, also creates ldap user if not in database
150 164
151 165 :param username: username
152 166 :param password: password
153 167 """
154 168
155 169 user_model = UserModel()
156 170 user = User.get_by_username(username)
157 171
158 172 log.debug('Authenticating user using RhodeCode account')
159 173 if user is not None and not user.ldap_dn:
160 174 if user.active:
161 175 if user.username == 'default' and user.active:
162 log.info('user %s authenticated correctly as anonymous user',
176 log.info('user %s authenticated correctly as anonymous user' %
163 177 username)
164 178 return True
165 179
166 180 elif user.username == username and check_password(password,
167 181 user.password):
168 log.info('user %s authenticated correctly', username)
182 log.info('user %s authenticated correctly' % username)
169 183 return True
170 184 else:
171 log.warning('user %s is disabled', username)
185 log.warning('user %s tried auth but is disabled' % username)
172 186
173 187 else:
174 188 log.debug('Regular authentication failed')
175 189 user_obj = User.get_by_username(username, case_insensitive=True)
176 190
177 191 if user_obj is not None and not user_obj.ldap_dn:
178 192 log.debug('this user already exists as non ldap')
179 193 return False
180 194
181 ldap_settings = RhodeCodeSettings.get_ldap_settings()
195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
182 196 #======================================================================
183 197 # FALLBACK TO LDAP AUTH IF ENABLE
184 198 #======================================================================
185 199 if str2bool(ldap_settings.get('ldap_active')):
186 200 log.debug("Authenticating user using ldap")
187 201 kwargs = {
188 202 'server': ldap_settings.get('ldap_host', ''),
189 203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
190 204 'port': ldap_settings.get('ldap_port'),
191 205 'bind_dn': ldap_settings.get('ldap_dn_user'),
192 206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
193 207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
194 208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
195 209 'ldap_filter': ldap_settings.get('ldap_filter'),
196 210 'search_scope': ldap_settings.get('ldap_search_scope'),
197 211 'attr_login': ldap_settings.get('ldap_attr_login'),
198 212 'ldap_version': 3,
199 213 }
200 214 log.debug('Checking for ldap authentication')
201 215 try:
202 216 aldap = AuthLdap(**kwargs)
203 217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
204 218 password)
205 log.debug('Got ldap DN response %s', user_dn)
219 log.debug('Got ldap DN response %s' % user_dn)
206 220
207 221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
208 222 .get(k), [''])[0]
209 223
210 224 user_attrs = {
211 225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
212 226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
213 227 'email': get_ldap_attr('ldap_attr_email'),
214 228 }
215 229
216 230 # don't store LDAP password since we don't need it. Override
217 231 # with some random generated password
218 232 _password = PasswordGenerator().gen_password(length=8)
219 233 # create this user on the fly if it doesn't exist in rhodecode
220 234 # database
221 235 if user_model.create_ldap(username, _password, user_dn,
222 236 user_attrs):
223 237 log.info('created new ldap user %s' % username)
224 238
239 Session.commit()
225 240 return True
226 241 except (LdapUsernameError, LdapPasswordError,):
227 242 pass
228 243 except (Exception,):
229 244 log.error(traceback.format_exc())
230 245 pass
231 246 return False
232 247
233 248
249 def login_container_auth(username):
250 user = User.get_by_username(username)
251 if user is None:
252 user_attrs = {
253 'name': username,
254 'lastname': None,
255 'email': None,
256 }
257 user = UserModel().create_for_container_auth(username, user_attrs)
258 if not user:
259 return None
260 log.info('User %s was created by container authentication' % username)
261
262 if not user.active:
263 return None
264
265 user.update_lastlogin()
266 Session.commit()
267
268 log.debug('User %s is now logged in by container authentication',
269 user.username)
270 return user
271
272
273 def get_container_username(environ, config):
274 username = None
275
276 if str2bool(config.get('container_auth_enabled', False)):
277 from paste.httpheaders import REMOTE_USER
278 username = REMOTE_USER(environ)
279
280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 username = environ.get('HTTP_X_FORWARDED_USER')
282
283 if username:
284 # Removing realm and domain from username
285 username = username.partition('@')[0]
286 username = username.rpartition('\\')[2]
287 log.debug('Received username %s from container' % username)
288
289 return username
290
291
292 class CookieStoreWrapper(object):
293
294 def __init__(self, cookie_store):
295 self.cookie_store = cookie_store
296
297 def __repr__(self):
298 return 'CookieStore<%s>' % (self.cookie_store)
299
300 def get(self, key, other=None):
301 if isinstance(self.cookie_store, dict):
302 return self.cookie_store.get(key, other)
303 elif isinstance(self.cookie_store, AuthUser):
304 return self.cookie_store.__dict__.get(key, other)
305
306
234 307 class AuthUser(object):
235 308 """
236 309 A simple object that handles all attributes of user in RhodeCode
237 310
238 311 It does lookup based on API key,given user, or user present in session
239 312 Then it fills all required information for such user. It also checks if
240 313 anonymous access is enabled and if so, it returns default user as logged
241 314 in
242 315 """
243 316
244 def __init__(self, user_id=None, api_key=None):
317 def __init__(self, user_id=None, api_key=None, username=None):
245 318
246 319 self.user_id = user_id
247 320 self.api_key = None
321 self.username = username
248 322
249 self.username = 'None'
250 323 self.name = ''
251 324 self.lastname = ''
252 325 self.email = ''
253 326 self.is_authenticated = False
254 327 self.admin = False
255 328 self.permissions = {}
256 329 self._api_key = api_key
257 330 self.propagate_data()
331 self._instance = None
258 332
259 333 def propagate_data(self):
260 334 user_model = UserModel()
261 self.anonymous_user = User.get_by_username('default')
335 self.anonymous_user = User.get_by_username('default', cache=True)
336 is_user_loaded = False
337
338 # try go get user by api key
262 339 if self._api_key and self._api_key != self.anonymous_user.api_key:
263 #try go get user by api key
264 log.debug('Auth User lookup by API KEY %s', self._api_key)
265 user_model.fill_data(self, api_key=self._api_key)
266 else:
267 log.debug('Auth User lookup by USER ID %s', self.user_id)
268 if self.user_id is not None \
269 and self.user_id != self.anonymous_user.user_id:
270 user_model.fill_data(self, user_id=self.user_id)
340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
342 # lookup by userid
343 elif (self.user_id is not None and
344 self.user_id != self.anonymous_user.user_id):
345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
347 # lookup by username
348 elif self.username and \
349 str2bool(config.get('container_auth_enabled', False)):
350
351 log.debug('Auth User lookup by USER NAME %s' % self.username)
352 dbuser = login_container_auth(self.username)
353 if dbuser is not None:
354 for k, v in dbuser.get_dict().items():
355 setattr(self, k, v)
356 self.set_authenticated()
357 is_user_loaded = True
358
359 if not is_user_loaded:
360 # if we cannot authenticate user try anonymous
361 if self.anonymous_user.active is True:
362 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
363 # then we set this user is logged in
364 self.is_authenticated = True
271 365 else:
272 if self.anonymous_user.active is True:
273 user_model.fill_data(self,
274 user_id=self.anonymous_user.user_id)
275 #then we set this user is logged in
276 self.is_authenticated = True
277 else:
278 self.is_authenticated = False
366 self.user_id = None
367 self.username = None
368 self.is_authenticated = False
279 369
280 log.debug('Auth User is now %s', self)
370 if not self.username:
371 self.username = 'None'
372
373 log.debug('Auth User is now %s' % self)
281 374 user_model.fill_perms(self)
282 375
283 376 @property
284 377 def is_admin(self):
285 378 return self.admin
286 379
287 @property
288 def full_contact(self):
289 return '%s %s <%s>' % (self.name, self.lastname, self.email)
290
291 380 def __repr__(self):
292 381 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
293 382 self.is_authenticated)
294 383
295 384 def set_authenticated(self, authenticated=True):
296
297 385 if self.user_id != self.anonymous_user.user_id:
298 386 self.is_authenticated = authenticated
299 387
388 def get_cookie_store(self):
389 return {'username': self.username,
390 'user_id': self.user_id,
391 'is_authenticated': self.is_authenticated}
392
393 @classmethod
394 def from_cookie_store(cls, cookie_store):
395 """
396 Creates AuthUser from a cookie store
397
398 :param cls:
399 :param cookie_store:
400 """
401 user_id = cookie_store.get('user_id')
402 username = cookie_store.get('username')
403 api_key = cookie_store.get('api_key')
404 return AuthUser(user_id, api_key, username)
405
300 406
301 407 def set_available_permissions(config):
302 """This function will propagate pylons globals with all available defined
408 """
409 This function will propagate pylons globals with all available defined
303 410 permission given in db. We don't want to check each time from db for new
304 411 permissions since adding a new permission also requires application restart
305 412 ie. to decorate new views with the newly created permission
306 413
307 414 :param config: current pylons config instance
308 415
309 416 """
310 417 log.info('getting information about all available permissions')
311 418 try:
312 sa = meta.Session()
419 sa = meta.Session
313 420 all_perms = sa.query(Permission).all()
314 except:
421 except Exception:
315 422 pass
316 423 finally:
317 424 meta.Session.remove()
318 425
319 426 config['available_permissions'] = [x.permission_name for x in all_perms]
320 427
321 428
322 429 #==============================================================================
323 430 # CHECK DECORATORS
324 431 #==============================================================================
325 432 class LoginRequired(object):
326 433 """
327 434 Must be logged in to execute this function else
328 435 redirect to login page
329 436
330 437 :param api_access: if enabled this checks only for valid auth token
331 438 and grants access based on valid token
332 439 """
333 440
334 441 def __init__(self, api_access=False):
335 442 self.api_access = api_access
336 443
337 444 def __call__(self, func):
338 445 return decorator(self.__wrapper, func)
339 446
340 447 def __wrapper(self, func, *fargs, **fkwargs):
341 448 cls = fargs[0]
342 449 user = cls.rhodecode_user
343 450
344 451 api_access_ok = False
345 452 if self.api_access:
346 log.debug('Checking API KEY access for %s', cls)
453 log.debug('Checking API KEY access for %s' % cls)
347 454 if user.api_key == request.GET.get('api_key'):
348 455 api_access_ok = True
349 456 else:
350 457 log.debug("API KEY token not valid")
351
352 log.debug('Checking if %s is authenticated @ %s', user.username, cls)
458 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
459 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
353 460 if user.is_authenticated or api_access_ok:
354 log.debug('user %s is authenticated', user.username)
461 log.info('user %s is authenticated and granted access to %s' % (
462 user.username, loc)
463 )
355 464 return func(*fargs, **fkwargs)
356 465 else:
357 log.warn('user %s NOT authenticated', user.username)
466 log.warn('user %s NOT authenticated on func: %s' % (
467 user, loc)
468 )
358 469 p = url.current()
359 470
360 log.debug('redirecting to login page with %s', p)
471 log.debug('redirecting to login page with %s' % p)
361 472 return redirect(url('login_home', came_from=p))
362 473
363 474
364 475 class NotAnonymous(object):
365 """Must be logged in to execute this function else
476 """
477 Must be logged in to execute this function else
366 478 redirect to login page"""
367 479
368 480 def __call__(self, func):
369 481 return decorator(self.__wrapper, func)
370 482
371 483 def __wrapper(self, func, *fargs, **fkwargs):
372 484 cls = fargs[0]
373 485 self.user = cls.rhodecode_user
374 486
375 log.debug('Checking if user is not anonymous @%s', cls)
487 log.debug('Checking if user is not anonymous @%s' % cls)
376 488
377 489 anonymous = self.user.username == 'default'
378 490
379 491 if anonymous:
380 492 p = url.current()
381 493
382 494 import rhodecode.lib.helpers as h
383 495 h.flash(_('You need to be a registered user to '
384 496 'perform this action'),
385 497 category='warning')
386 498 return redirect(url('login_home', came_from=p))
387 499 else:
388 500 return func(*fargs, **fkwargs)
389 501
390 502
391 503 class PermsDecorator(object):
392 504 """Base class for controller decorators"""
393 505
394 506 def __init__(self, *required_perms):
395 507 available_perms = config['available_permissions']
396 508 for perm in required_perms:
397 509 if perm not in available_perms:
398 510 raise Exception("'%s' permission is not defined" % perm)
399 511 self.required_perms = set(required_perms)
400 512 self.user_perms = None
401 513
402 514 def __call__(self, func):
403 515 return decorator(self.__wrapper, func)
404 516
405 517 def __wrapper(self, func, *fargs, **fkwargs):
406 518 cls = fargs[0]
407 519 self.user = cls.rhodecode_user
408 520 self.user_perms = self.user.permissions
409 521 log.debug('checking %s permissions %s for %s %s',
410 522 self.__class__.__name__, self.required_perms, cls,
411 523 self.user)
412 524
413 525 if self.check_permissions():
414 log.debug('Permission granted for %s %s', cls, self.user)
526 log.debug('Permission granted for %s %s' % (cls, self.user))
415 527 return func(*fargs, **fkwargs)
416 528
417 529 else:
418 log.warning('Permission denied for %s %s', cls, self.user)
419
420
530 log.debug('Permission denied for %s %s' % (cls, self.user))
421 531 anonymous = self.user.username == 'default'
422 532
423 533 if anonymous:
424 534 p = url.current()
425 535
426 536 import rhodecode.lib.helpers as h
427 537 h.flash(_('You need to be a signed in to '
428 538 'view this page'),
429 539 category='warning')
430 540 return redirect(url('login_home', came_from=p))
431 541
432 542 else:
433 #redirect with forbidden ret code
543 # redirect with forbidden ret code
434 544 return abort(403)
435 545
436 546 def check_permissions(self):
437 547 """Dummy function for overriding"""
438 548 raise Exception('You have to write this function in child class')
439 549
440 550
441 551 class HasPermissionAllDecorator(PermsDecorator):
442 """Checks for access permission for all given predicates. All of them
552 """
553 Checks for access permission for all given predicates. All of them
443 554 have to be meet in order to fulfill the request
444 555 """
445 556
446 557 def check_permissions(self):
447 558 if self.required_perms.issubset(self.user_perms.get('global')):
448 559 return True
449 560 return False
450 561
451 562
452 563 class HasPermissionAnyDecorator(PermsDecorator):
453 """Checks for access permission for any of given predicates. In order to
564 """
565 Checks for access permission for any of given predicates. In order to
454 566 fulfill the request any of predicates must be meet
455 567 """
456 568
457 569 def check_permissions(self):
458 570 if self.required_perms.intersection(self.user_perms.get('global')):
459 571 return True
460 572 return False
461 573
462 574
463 575 class HasRepoPermissionAllDecorator(PermsDecorator):
464 """Checks for access permission for all given predicates for specific
576 """
577 Checks for access permission for all given predicates for specific
465 578 repository. All of them have to be meet in order to fulfill the request
466 579 """
467 580
468 581 def check_permissions(self):
469 582 repo_name = get_repo_slug(request)
470 583 try:
471 584 user_perms = set([self.user_perms['repositories'][repo_name]])
472 585 except KeyError:
473 586 return False
474 587 if self.required_perms.issubset(user_perms):
475 588 return True
476 589 return False
477 590
478 591
479 592 class HasRepoPermissionAnyDecorator(PermsDecorator):
480 """Checks for access permission for any of given predicates for specific
593 """
594 Checks for access permission for any of given predicates for specific
481 595 repository. In order to fulfill the request any of predicates must be meet
482 596 """
483 597
484 598 def check_permissions(self):
485 599 repo_name = get_repo_slug(request)
486 600
487 601 try:
488 602 user_perms = set([self.user_perms['repositories'][repo_name]])
489 603 except KeyError:
490 604 return False
491 605 if self.required_perms.intersection(user_perms):
492 606 return True
493 607 return False
494 608
495 609
610 class HasReposGroupPermissionAllDecorator(PermsDecorator):
611 """
612 Checks for access permission for all given predicates for specific
613 repository. All of them have to be meet in order to fulfill the request
614 """
615
616 def check_permissions(self):
617 group_name = get_repos_group_slug(request)
618 try:
619 user_perms = set([self.user_perms['repositories_groups'][group_name]])
620 except KeyError:
621 return False
622 if self.required_perms.issubset(user_perms):
623 return True
624 return False
625
626
627 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
628 """
629 Checks for access permission for any of given predicates for specific
630 repository. In order to fulfill the request any of predicates must be meet
631 """
632
633 def check_permissions(self):
634 group_name = get_repos_group_slug(request)
635
636 try:
637 user_perms = set([self.user_perms['repositories_groups'][group_name]])
638 except KeyError:
639 return False
640 if self.required_perms.intersection(user_perms):
641 return True
642 return False
643
644
496 645 #==============================================================================
497 646 # CHECK FUNCTIONS
498 647 #==============================================================================
499 648 class PermsFunction(object):
500 649 """Base function for other check functions"""
501 650
502 651 def __init__(self, *perms):
503 652 available_perms = config['available_permissions']
504 653
505 654 for perm in perms:
506 655 if perm not in available_perms:
507 656 raise Exception("'%s' permission in not defined" % perm)
508 657 self.required_perms = set(perms)
509 658 self.user_perms = None
510 659 self.granted_for = ''
511 660 self.repo_name = None
512 661
513 662 def __call__(self, check_Location=''):
514 user = session.get('rhodecode_user', False)
663 user = request.user
515 664 if not user:
516 665 return False
517 666 self.user_perms = user.permissions
518 667 self.granted_for = user
519 668 log.debug('checking %s %s %s', self.__class__.__name__,
520 669 self.required_perms, user)
521 670
522 671 if self.check_permissions():
523 672 log.debug('Permission granted %s @ %s', self.granted_for,
524 673 check_Location or 'unspecified location')
525 674 return True
526 675
527 676 else:
528 log.warning('Permission denied for %s @ %s', self.granted_for,
677 log.debug('Permission denied for %s @ %s', self.granted_for,
529 678 check_Location or 'unspecified location')
530 679 return False
531 680
532 681 def check_permissions(self):
533 682 """Dummy function for overriding"""
534 683 raise Exception('You have to write this function in child class')
535 684
536 685
537 686 class HasPermissionAll(PermsFunction):
538 687 def check_permissions(self):
539 688 if self.required_perms.issubset(self.user_perms.get('global')):
540 689 return True
541 690 return False
542 691
543 692
544 693 class HasPermissionAny(PermsFunction):
545 694 def check_permissions(self):
546 695 if self.required_perms.intersection(self.user_perms.get('global')):
547 696 return True
548 697 return False
549 698
550 699
551 700 class HasRepoPermissionAll(PermsFunction):
552 701
553 702 def __call__(self, repo_name=None, check_Location=''):
554 703 self.repo_name = repo_name
555 704 return super(HasRepoPermissionAll, self).__call__(check_Location)
556 705
557 706 def check_permissions(self):
558 707 if not self.repo_name:
559 708 self.repo_name = get_repo_slug(request)
560 709
561 710 try:
562 self.user_perms = set([self.user_perms['reposit'
563 'ories'][self.repo_name]])
711 self.user_perms = set(
712 [self.user_perms['repositories'][self.repo_name]]
713 )
564 714 except KeyError:
565 715 return False
566 716 self.granted_for = self.repo_name
567 717 if self.required_perms.issubset(self.user_perms):
568 718 return True
569 719 return False
570 720
571 721
572 722 class HasRepoPermissionAny(PermsFunction):
573 723
574 724 def __call__(self, repo_name=None, check_Location=''):
575 725 self.repo_name = repo_name
576 726 return super(HasRepoPermissionAny, self).__call__(check_Location)
577 727
578 728 def check_permissions(self):
579 729 if not self.repo_name:
580 730 self.repo_name = get_repo_slug(request)
581 731
582 732 try:
583 self.user_perms = set([self.user_perms['reposi'
584 'tories'][self.repo_name]])
733 self.user_perms = set(
734 [self.user_perms['repositories'][self.repo_name]]
735 )
585 736 except KeyError:
586 737 return False
587 738 self.granted_for = self.repo_name
588 739 if self.required_perms.intersection(self.user_perms):
589 740 return True
590 741 return False
591 742
592 743
744 class HasReposGroupPermissionAny(PermsFunction):
745 def __call__(self, group_name=None, check_Location=''):
746 self.group_name = group_name
747 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
748
749 def check_permissions(self):
750 try:
751 self.user_perms = set(
752 [self.user_perms['repositories_groups'][self.group_name]]
753 )
754 except KeyError:
755 return False
756 self.granted_for = self.repo_name
757 if self.required_perms.intersection(self.user_perms):
758 return True
759 return False
760
761
762 class HasReposGroupPermissionAll(PermsFunction):
763 def __call__(self, group_name=None, check_Location=''):
764 self.group_name = group_name
765 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
766
767 def check_permissions(self):
768 try:
769 self.user_perms = set(
770 [self.user_perms['repositories_groups'][self.group_name]]
771 )
772 except KeyError:
773 return False
774 self.granted_for = self.repo_name
775 if self.required_perms.issubset(self.user_perms):
776 return True
777 return False
778
779
593 780 #==============================================================================
594 781 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
595 782 #==============================================================================
596 783 class HasPermissionAnyMiddleware(object):
597 784 def __init__(self, *perms):
598 785 self.required_perms = set(perms)
599 786
600 787 def __call__(self, user, repo_name):
601 788 usr = AuthUser(user.user_id)
602 789 try:
603 790 self.user_perms = set([usr.permissions['repositories'][repo_name]])
604 791 except:
605 792 self.user_perms = set()
606 793 self.granted_for = ''
607 794 self.username = user.username
608 795 self.repo_name = repo_name
609 796 return self.check_permissions()
610 797
611 798 def check_permissions(self):
612 799 log.debug('checking mercurial protocol '
613 800 'permissions %s for user:%s repository:%s', self.user_perms,
614 801 self.username, self.repo_name)
615 802 if self.required_perms.intersection(self.user_perms):
616 803 log.debug('permission granted')
617 804 return True
618 805 log.debug('permission denied')
619 806 return False
@@ -1,151 +1,159 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changelog
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode authentication library for LDAP
7 7
8 8 :created_on: Created on Nov 17, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
29 29 LdapPasswordError
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 try:
35 35 import ldap
36 36 except ImportError:
37 37 # means that python-ldap is not installed
38 38 pass
39 39
40 40
41 41 class AuthLdap(object):
42 42
43 43 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
44 44 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
45 45 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
46 search_scope='SUBTREE',
47 attr_login='uid'):
46 search_scope='SUBTREE', attr_login='uid'):
48 47 self.ldap_version = ldap_version
49 48 ldap_server_type = 'ldap'
50 49
51 50 self.TLS_KIND = tls_kind
52 51
53 52 if self.TLS_KIND == 'LDAPS':
54 53 port = port or 689
55 54 ldap_server_type = ldap_server_type + 's'
56
55
57 56 OPT_X_TLS_DEMAND = 2
58 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
57 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
59 58 OPT_X_TLS_DEMAND)
60 59 self.LDAP_SERVER_ADDRESS = server
61 60 self.LDAP_SERVER_PORT = port
62 61
63 #USE FOR READ ONLY BIND TO LDAP SERVER
62 # USE FOR READ ONLY BIND TO LDAP SERVER
64 63 self.LDAP_BIND_DN = bind_dn
65 64 self.LDAP_BIND_PASS = bind_pass
66 65
67 66 self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
68 67 self.LDAP_SERVER_ADDRESS,
69 68 self.LDAP_SERVER_PORT)
70 69
71 70 self.BASE_DN = base_dn
72 71 self.LDAP_FILTER = ldap_filter
73 72 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
74 73 self.attr_login = attr_login
75 74
76 75 def authenticate_ldap(self, username, password):
77 """Authenticate a user via LDAP and return his/her LDAP properties.
76 """
77 Authenticate a user via LDAP and return his/her LDAP properties.
78 78
79 79 Raises AuthenticationError if the credentials are rejected, or
80 80 EnvironmentError if the LDAP server can't be reached.
81 81
82 82 :param username: username
83 83 :param password: password
84 84 """
85 85
86 86 from rhodecode.lib.helpers import chop_at
87 87
88 88 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
89 89
90 if not password:
91 log.debug("Attempt to authenticate LDAP user "
92 "with blank password rejected.")
93 raise LdapPasswordError()
90 94 if "," in username:
91 95 raise LdapUsernameError("invalid character in username: ,")
92 96 try:
93 if hasattr(ldap,'OPT_X_TLS_CACERTDIR'):
94 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
97 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
98 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
95 99 '/etc/openldap/cacerts')
96 100 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
97 101 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
98 102 ldap.set_option(ldap.OPT_TIMEOUT, 20)
99 103 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
100 104 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
101 105 if self.TLS_KIND != 'PLAIN':
102 106 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
103 107 server = ldap.initialize(self.LDAP_SERVER)
104 108 if self.ldap_version == 2:
105 109 server.protocol = ldap.VERSION2
106 110 else:
107 111 server.protocol = ldap.VERSION3
108 112
109 113 if self.TLS_KIND == 'START_TLS':
110 114 server.start_tls_s()
111 115
112 116 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
113 117 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
114 118
115 filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
119 filter_ = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
116 120 username)
117 log.debug("Authenticating %r filt %s at %s", self.BASE_DN,
118 filt, self.LDAP_SERVER)
121 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
122 filter_, self.LDAP_SERVER)
119 123 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE,
120 filt)
124 filter_)
121 125
122 126 if not lobjects:
123 127 raise ldap.NO_SUCH_OBJECT()
124 128
125 129 for (dn, _attrs) in lobjects:
126 130 if dn is None:
127 131 continue
128 132
129 133 try:
134 log.debug('Trying simple bind with %s' % dn)
130 135 server.simple_bind_s(dn, password)
131 136 attrs = server.search_ext_s(dn, ldap.SCOPE_BASE,
132 137 '(objectClass=*)')[0][1]
133 138 break
134 139
135 except ldap.INVALID_CREDENTIALS, e:
136 log.debug("LDAP rejected password for user '%s' (%s): %s",
137 uid, username, dn)
140 except ldap.INVALID_CREDENTIALS:
141 log.debug(
142 "LDAP rejected password for user '%s' (%s): %s" % (
143 uid, username, dn
144 )
145 )
138 146
139 147 else:
140 148 log.debug("No matching LDAP objects for authentication "
141 149 "of '%s' (%s)", uid, username)
142 150 raise LdapPasswordError()
143 151
144 except ldap.NO_SUCH_OBJECT, e:
145 log.debug("LDAP says no such user '%s' (%s)", uid, username)
152 except ldap.NO_SUCH_OBJECT:
153 log.debug("LDAP says no such user '%s' (%s)" % (uid, username))
146 154 raise LdapUsernameError()
147 except ldap.SERVER_DOWN, e:
155 except ldap.SERVER_DOWN:
148 156 raise LdapConnectionError("LDAP can't access "
149 157 "authentication server")
150 158
151 159 return (dn, attrs)
@@ -1,101 +1,102 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.backup_manager
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Mercurial repositories backup manager, it allows to backups all
6 Mercurial repositories backup manager, it allows to backups all
7 7 repositories and send it to backup server using RSA key via ssh.
8 8
9 9 :created_on: Feb 28, 2010
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 12 :license: GPLv3, see COPYING for more details.
12 13 """
13 14 # This program is free software: you can redistribute it and/or modify
14 15 # it under the terms of the GNU General Public License as published by
15 16 # the Free Software Foundation, either version 3 of the License, or
16 17 # (at your option) any later version.
17 18 #
18 19 # This program is distributed in the hope that it will be useful,
19 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 22 # GNU General Public License for more details.
22 23 #
23 24 # You should have received a copy of the GNU General Public License
24 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 26
26 27 import os
27 28 import sys
28 29
29 30 import logging
30 31 import tarfile
31 32 import datetime
32 33 import subprocess
33 34
34 35 logging.basicConfig(level=logging.DEBUG,
35 36 format="%(asctime)s %(levelname)-5.5s %(message)s")
36 37
37 38
38 39 class BackupManager(object):
39 40 def __init__(self, repos_location, rsa_key, backup_server):
40 41 today = datetime.datetime.now().weekday() + 1
41 42 self.backup_file_name = "mercurial_repos.%s.tar.gz" % today
42 43
43 44 self.id_rsa_path = self.get_id_rsa(rsa_key)
44 45 self.repos_path = self.get_repos_path(repos_location)
45 46 self.backup_server = backup_server
46 47
47 48 self.backup_file_path = '/tmp'
48 49
49 50 logging.info('starting backup for %s', self.repos_path)
50 51 logging.info('backup target %s', self.backup_file_path)
51 52
52 53 def get_id_rsa(self, rsa_key):
53 54 if not os.path.isfile(rsa_key):
54 55 logging.error('Could not load id_rsa key file in %s', rsa_key)
55 56 sys.exit()
56 57 return rsa_key
57 58
58 59 def get_repos_path(self, path):
59 60 if not os.path.isdir(path):
60 61 logging.error('Wrong location for repositories in %s', path)
61 62 sys.exit()
62 63 return path
63 64
64 65 def backup_repos(self):
65 66 bckp_file = os.path.join(self.backup_file_path, self.backup_file_name)
66 67 tar = tarfile.open(bckp_file, "w:gz")
67 68
68 69 for dir_name in os.listdir(self.repos_path):
69 70 logging.info('backing up %s', dir_name)
70 71 tar.add(os.path.join(self.repos_path, dir_name), dir_name)
71 72 tar.close()
72 73 logging.info('finished backup of mercurial repositories')
73 74
74 75 def transfer_files(self):
75 76 params = {
76 77 'id_rsa_key': self.id_rsa_path,
77 78 'backup_file': os.path.join(self.backup_file_path,
78 79 self.backup_file_name),
79 80 'backup_server': self.backup_server
80 81 }
81 82 cmd = ['scp', '-l', '40000', '-i', '%(id_rsa_key)s' % params,
82 83 '%(backup_file)s' % params,
83 84 '%(backup_server)s' % params]
84 85
85 86 subprocess.call(cmd)
86 87 logging.info('Transfered file %s to %s', self.backup_file_name, cmd[4])
87 88
88 89 def rm_file(self):
89 90 logging.info('Removing file %s', self.backup_file_name)
90 91 os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
91 92
92 93 if __name__ == "__main__":
93 94
94 95 repo_location = '/home/repo_path'
95 96 backup_server = 'root@192.168.1.100:/backups/mercurial'
96 97 rsa_key = '/home/id_rsa'
97 98
98 99 B_MANAGER = BackupManager(repo_location, rsa_key, backup_server)
99 100 B_MANAGER.backup_repos()
100 101 B_MANAGER.transfer_files()
101 102 B_MANAGER.rm_file()
@@ -1,83 +1,184 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 import time
7 import traceback
8
9 from paste.auth.basic import AuthBasicAuthenticator
6 10
7 11 from pylons import config, tmpl_context as c, request, session, url
8 12 from pylons.controllers import WSGIController
9 13 from pylons.controllers.util import redirect
10 14 from pylons.templating import render_mako as render
11 15
12 from rhodecode import __version__
13 from rhodecode.lib import str2bool
14 from rhodecode.lib.auth import AuthUser
15 from rhodecode.lib.utils import get_repo_slug
16 from rhodecode import __version__, BACKENDS
17
18 from rhodecode.lib import str2bool, safe_unicode
19 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
20 HasPermissionAnyMiddleware, CookieStoreWrapper
21 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
16 22 from rhodecode.model import meta
23
24 from rhodecode.model.db import Repository
25 from rhodecode.model.notification import NotificationModel
17 26 from rhodecode.model.scm import ScmModel
18 from rhodecode import BACKENDS
19 from rhodecode.model.db import Repository
20 27
21 28 log = logging.getLogger(__name__)
22 29
30
31 class BaseVCSController(object):
32
33 def __init__(self, application, config):
34 self.application = application
35 self.config = config
36 # base path of repo locations
37 self.basepath = self.config['base_path']
38 #authenticate this mercurial request using authfunc
39 self.authenticate = AuthBasicAuthenticator('', authfunc)
40 self.ipaddr = '0.0.0.0'
41
42 def _handle_request(self, environ, start_response):
43 raise NotImplementedError()
44
45 def _get_by_id(self, repo_name):
46 """
47 Get's a special pattern _<ID> from clone url and tries to replace it
48 with a repository_name for support of _<ID> non changable urls
49
50 :param repo_name:
51 """
52 try:
53 data = repo_name.split('/')
54 if len(data) >= 2:
55 by_id = data[1].split('_')
56 if len(by_id) == 2 and by_id[1].isdigit():
57 _repo_name = Repository.get(by_id[1]).repo_name
58 data[1] = _repo_name
59 except:
60 log.debug('Failed to extract repo_name from id %s' % (
61 traceback.format_exc()
62 )
63 )
64
65 return '/'.join(data)
66
67 def _invalidate_cache(self, repo_name):
68 """
69 Set's cache for this repository for invalidation on next access
70
71 :param repo_name: full repo name, also a cache key
72 """
73 invalidate_cache('get_repo_cached_%s' % repo_name)
74
75 def _check_permission(self, action, user, repo_name):
76 """
77 Checks permissions using action (push/pull) user and repository
78 name
79
80 :param action: push or pull action
81 :param user: user instance
82 :param repo_name: repository name
83 """
84 if action == 'push':
85 if not HasPermissionAnyMiddleware('repository.write',
86 'repository.admin')(user,
87 repo_name):
88 return False
89
90 else:
91 #any other action need at least read permission
92 if not HasPermissionAnyMiddleware('repository.read',
93 'repository.write',
94 'repository.admin')(user,
95 repo_name):
96 return False
97
98 return True
99
100 def __call__(self, environ, start_response):
101 start = time.time()
102 try:
103 return self._handle_request(environ, start_response)
104 finally:
105 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
106 log.debug('Request time: %.3fs' % (time.time() - start))
107 meta.Session.remove()
108
109
23 110 class BaseController(WSGIController):
24 111
25 112 def __before__(self):
26 113 c.rhodecode_version = __version__
114 c.rhodecode_instanceid = config.get('instance_id')
27 115 c.rhodecode_name = config.get('rhodecode_title')
28 116 c.use_gravatar = str2bool(config.get('use_gravatar'))
29 117 c.ga_code = config.get('rhodecode_ga_code')
30 118 c.repo_name = get_repo_slug(request)
31 119 c.backends = BACKENDS.keys()
120 c.unread_notifications = NotificationModel()\
121 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
32 122 self.cut_off_limit = int(config.get('cut_off_limit'))
33 123
34 self.sa = meta.Session()
124 self.sa = meta.Session
35 125 self.scm_model = ScmModel(self.sa)
36 126
37 127 def __call__(self, environ, start_response):
38 128 """Invoke the Controller"""
39 129 # WSGIController.__call__ dispatches to the Controller method
40 130 # the request is routed to. This routing information is
41 131 # available in environ['pylons.routes_dict']
132 start = time.time()
42 133 try:
43 # putting this here makes sure that we update permissions each time
134 # make sure that we update permissions each time we call controller
44 135 api_key = request.GET.get('api_key')
45 user_id = getattr(session.get('rhodecode_user'), 'user_id', None)
46 self.rhodecode_user = c.rhodecode_user = AuthUser(user_id, api_key)
47 self.rhodecode_user.set_authenticated(
48 getattr(session.get('rhodecode_user'),
49 'is_authenticated', False))
50 session['rhodecode_user'] = self.rhodecode_user
51 session.save()
136 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
137 user_id = cookie_store.get('user_id', None)
138 username = get_container_username(environ, config)
139
140 auth_user = AuthUser(user_id, api_key, username)
141 request.user = auth_user
142 self.rhodecode_user = c.rhodecode_user = auth_user
143 if not self.rhodecode_user.is_authenticated and \
144 self.rhodecode_user.user_id is not None:
145 self.rhodecode_user.set_authenticated(
146 cookie_store.get('is_authenticated')
147 )
148 log.info('User: %s accessed %s' % (
149 auth_user, safe_unicode(environ.get('PATH_INFO')))
150 )
52 151 return WSGIController.__call__(self, environ, start_response)
53 152 finally:
153 log.info('Request to %s time: %.3fs' % (
154 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
155 )
54 156 meta.Session.remove()
55 157
56 158
57 159 class BaseRepoController(BaseController):
58 160 """
59 161 Base class for controllers responsible for loading all needed data for
60 162 repository loaded items are
61 163
62 164 c.rhodecode_repo: instance of scm repository
63 165 c.rhodecode_db_repo: instance of db
64 166 c.repository_followers: number of followers
65 167 c.repository_forks: number of forks
66 168 """
67 169
68 170 def __before__(self):
69 171 super(BaseRepoController, self).__before__()
70 172 if c.repo_name:
71 173
72 174 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
73 175 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
74 176
75 177 if c.rhodecode_repo is None:
76 178 log.error('%s this repository is present in database but it '
77 179 'cannot be created as an scm instance', c.repo_name)
78 180
79 181 redirect(url('home'))
80 182
81 183 c.repository_followers = self.scm_model.get_followers(c.repo_name)
82 184 c.repository_forks = self.scm_model.get_forks(c.repo_name)
83
@@ -1,289 +1,299 b''
1 1 """caching_query.py
2 2
3 3 Represent persistence structures which allow the usage of
4 4 Beaker caching with SQLAlchemy.
5 5
6 6 The three new concepts introduced here are:
7 7
8 8 * CachingQuery - a Query subclass that caches and
9 9 retrieves results in/from Beaker.
10 10 * FromCache - a query option that establishes caching
11 11 parameters on a Query
12 12 * RelationshipCache - a variant of FromCache which is specific
13 13 to a query invoked during a lazy load.
14 14 * _params_from_query - extracts value parameters from
15 15 a Query.
16 16
17 17 The rest of what's here are standard SQLAlchemy and
18 18 Beaker constructs.
19 19
20 20 """
21 21 import beaker
22 22 from beaker.exceptions import BeakerException
23 23
24 24 from sqlalchemy.orm.interfaces import MapperOption
25 25 from sqlalchemy.orm.query import Query
26 26 from sqlalchemy.sql import visitors
27 27
28 28
29 29 class CachingQuery(Query):
30 30 """A Query subclass which optionally loads full results from a Beaker
31 31 cache region.
32 32
33 33 The CachingQuery stores additional state that allows it to consult
34 34 a Beaker cache before accessing the database:
35 35
36 36 * A "region", which is a cache region argument passed to a
37 37 Beaker CacheManager, specifies a particular cache configuration
38 38 (including backend implementation, expiration times, etc.)
39 39 * A "namespace", which is a qualifying name that identifies a
40 40 group of keys within the cache. A query that filters on a name
41 41 might use the name "by_name", a query that filters on a date range
42 42 to a joined table might use the name "related_date_range".
43 43
44 44 When the above state is present, a Beaker cache is retrieved.
45 45
46 46 The "namespace" name is first concatenated with
47 47 a string composed of the individual entities and columns the Query
48 48 requests, i.e. such as ``Query(User.id, User.name)``.
49 49
50 50 The Beaker cache is then loaded from the cache manager based
51 51 on the region and composed namespace. The key within the cache
52 52 itself is then constructed against the bind parameters specified
53 53 by this query, which are usually literals defined in the
54 54 WHERE clause.
55 55
56 56 The FromCache and RelationshipCache mapper options below represent
57 57 the "public" method of configuring this state upon the CachingQuery.
58 58
59 59 """
60 60
61 61 def __init__(self, manager, *args, **kw):
62 62 self.cache_manager = manager
63 63 Query.__init__(self, *args, **kw)
64 64
65 65 def __iter__(self):
66 66 """override __iter__ to pull results from Beaker
67 67 if particular attributes have been configured.
68 68
69 69 Note that this approach does *not* detach the loaded objects from
70 70 the current session. If the cache backend is an in-process cache
71 71 (like "memory") and lives beyond the scope of the current session's
72 72 transaction, those objects may be expired. The method here can be
73 73 modified to first expunge() each loaded item from the current
74 74 session before returning the list of items, so that the items
75 75 in the cache are not the same ones in the current Session.
76 76
77 77 """
78 78 if hasattr(self, '_cache_parameters'):
79 79 return self.get_value(createfunc=lambda:
80 80 list(Query.__iter__(self)))
81 81 else:
82 82 return Query.__iter__(self)
83 83
84 84 def invalidate(self):
85 85 """Invalidate the value represented by this Query."""
86 86
87 87 cache, cache_key = _get_cache_parameters(self)
88 88 cache.remove(cache_key)
89 89
90 90 def get_value(self, merge=True, createfunc=None):
91 91 """Return the value from the cache for this query.
92 92
93 93 Raise KeyError if no value present and no
94 94 createfunc specified.
95 95
96 96 """
97 97 cache, cache_key = _get_cache_parameters(self)
98 98 ret = cache.get_value(cache_key, createfunc=createfunc)
99 99 if merge:
100 100 ret = self.merge_result(ret, load=False)
101 101 return ret
102 102
103 103 def set_value(self, value):
104 104 """Set the value in the cache for this query."""
105 105
106 106 cache, cache_key = _get_cache_parameters(self)
107 107 cache.put(cache_key, value)
108 108
109 109
110 110 def query_callable(manager, query_cls=CachingQuery):
111 111 def query(*arg, **kw):
112 112 return query_cls(manager, *arg, **kw)
113 113 return query
114 114
115 115
116 116 def get_cache_region(name, region):
117 117 if region not in beaker.cache.cache_regions:
118 118 raise BeakerException('Cache region `%s` not configured '
119 119 'Check if proper cache settings are in the .ini files' % region)
120 120 kw = beaker.cache.cache_regions[region]
121 121 return beaker.cache.Cache._get_cache(name, kw)
122 122
123 123
124 124 def _get_cache_parameters(query):
125 125 """For a query with cache_region and cache_namespace configured,
126 126 return the correspoinding Cache instance and cache key, based
127 127 on this query's current criterion and parameter values.
128 128
129 129 """
130 130 if not hasattr(query, '_cache_parameters'):
131 131 raise ValueError("This Query does not have caching "
132 132 "parameters configured.")
133 133
134 134 region, namespace, cache_key = query._cache_parameters
135 135
136 136 namespace = _namespace_from_query(namespace, query)
137 137
138 138 if cache_key is None:
139 139 # cache key - the value arguments from this query's parameters.
140 args = _params_from_query(query)
141 cache_key = " ".join([str(x) for x in args])
140 args = [str(x) for x in _params_from_query(query)]
141 args.extend(filter(lambda k:k not in ['None', None, u'None'],
142 [str(query._limit), str(query._offset)]))
143 cache_key = " ".join(args)
144
145 if cache_key is None:
146 raise Exception('Cache key cannot be None')
142 147
143 148 # get cache
144 149 #cache = query.cache_manager.get_cache_region(namespace, region)
145 150 cache = get_cache_region(namespace, region)
146 151 # optional - hash the cache_key too for consistent length
147 152 # import uuid
148 153 # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
149 154
150 155 return cache, cache_key
151 156
152 157
153 158 def _namespace_from_query(namespace, query):
154 159 # cache namespace - the token handed in by the
155 160 # option + class we're querying against
156 161 namespace = " ".join([namespace] + [str(x) for x in query._entities])
157 162
158 163 # memcached wants this
159 164 namespace = namespace.replace(' ', '_')
160 165
161 166 return namespace
162 167
163 168
164 169 def _set_cache_parameters(query, region, namespace, cache_key):
165 170
166 171 if hasattr(query, '_cache_parameters'):
167 172 region, namespace, cache_key = query._cache_parameters
168 173 raise ValueError("This query is already configured "
169 174 "for region %r namespace %r" %
170 175 (region, namespace)
171 176 )
172 177 query._cache_parameters = region, namespace, cache_key
173 178
174 179
175 180 class FromCache(MapperOption):
176 181 """Specifies that a Query should load results from a cache."""
177 182
178 183 propagate_to_loaders = False
179 184
180 185 def __init__(self, region, namespace, cache_key=None):
181 186 """Construct a new FromCache.
182 187
183 188 :param region: the cache region. Should be a
184 189 region configured in the Beaker CacheManager.
185 190
186 191 :param namespace: the cache namespace. Should
187 192 be a name uniquely describing the target Query's
188 193 lexical structure.
189 194
190 195 :param cache_key: optional. A string cache key
191 196 that will serve as the key to the query. Use this
192 197 if your query has a huge amount of parameters (such
193 198 as when using in_()) which correspond more simply to
194 199 some other identifier.
195 200
196 201 """
197 202 self.region = region
198 203 self.namespace = namespace
199 204 self.cache_key = cache_key
200 205
201 206 def process_query(self, query):
202 207 """Process a Query during normal loading operation."""
203 208
204 209 _set_cache_parameters(query, self.region, self.namespace,
205 210 self.cache_key)
206 211
207 212
208 213 class RelationshipCache(MapperOption):
209 214 """Specifies that a Query as called within a "lazy load"
210 215 should load results from a cache."""
211 216
212 217 propagate_to_loaders = True
213 218
214 219 def __init__(self, region, namespace, attribute):
215 220 """Construct a new RelationshipCache.
216 221
217 222 :param region: the cache region. Should be a
218 223 region configured in the Beaker CacheManager.
219 224
220 225 :param namespace: the cache namespace. Should
221 226 be a name uniquely describing the target Query's
222 227 lexical structure.
223 228
224 229 :param attribute: A Class.attribute which
225 230 indicates a particular class relationship() whose
226 231 lazy loader should be pulled from the cache.
227 232
228 233 """
229 234 self.region = region
230 235 self.namespace = namespace
231 236 self._relationship_options = {
232 237 (attribute.property.parent.class_, attribute.property.key): self
233 238 }
234 239
235 240 def process_query_conditionally(self, query):
236 241 """Process a Query that is used within a lazy loader.
237 242
238 243 (the process_query_conditionally() method is a SQLAlchemy
239 244 hook invoked only within lazyload.)
240 245
241 246 """
242 247 if query._current_path:
243 248 mapper, key = query._current_path[-2:]
244 249
245 250 for cls in mapper.class_.__mro__:
246 251 if (cls, key) in self._relationship_options:
247 252 relationship_option = \
248 253 self._relationship_options[(cls, key)]
249 254 _set_cache_parameters(
250 255 query,
251 256 relationship_option.region,
252 257 relationship_option.namespace,
253 258 None)
254 259
255 260 def and_(self, option):
256 261 """Chain another RelationshipCache option to this one.
257 262
258 263 While many RelationshipCache objects can be specified on a single
259 264 Query separately, chaining them together allows for a more efficient
260 265 lookup during load.
261 266
262 267 """
263 268 self._relationship_options.update(option._relationship_options)
264 269 return self
265 270
266 271
267 272 def _params_from_query(query):
268 273 """Pull the bind parameter values from a query.
269 274
270 275 This takes into account any scalar attribute bindparam set up.
271 276
272 277 E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
273 278 would return [5, 7].
274 279
275 280 """
276 281 v = []
277 282 def visit_bindparam(bind):
278 value = query._params.get(bind.key, bind.value)
279 283
280 # lazyloader may dig a callable in here, intended
281 # to late-evaluate params after autoflush is called.
282 # convert to a scalar value.
283 if callable(value):
284 value = value()
284 if bind.key in query._params:
285 value = query._params[bind.key]
286 elif bind.callable:
287 # lazyloader may dig a callable in here, intended
288 # to late-evaluate params after autoflush is called.
289 # convert to a scalar value.
290 value = bind.callable()
291 else:
292 value = bind.value
285 293
286 294 v.append(value)
287 295 if query._criterion is not None:
288 296 visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
297 for f in query._from_obj:
298 visitors.traverse(f, {}, {'bindparam':visit_bindparam})
289 299 return v
@@ -1,133 +1,128 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 celery libs for RhodeCode
7 7
8 8 :created_on: Nov 27, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import sys
28 28 import socket
29 29 import traceback
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32 from pylons import config
33 33
34 34 from hashlib import md5
35 35 from decorator import decorator
36 36
37 from vcs.utils.lazy import LazyProperty
38
37 from rhodecode.lib.vcs.utils.lazy import LazyProperty
38 from rhodecode import CELERY_ON
39 39 from rhodecode.lib import str2bool, safe_str
40 40 from rhodecode.lib.pidlock import DaemonLock, LockHeld
41 41 from rhodecode.model import init_model
42 42 from rhodecode.model import meta
43 43 from rhodecode.model.db import Statistics, Repository, User
44 44
45 45 from sqlalchemy import engine_from_config
46 46
47 47 from celery.messaging import establish_connection
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 try:
52 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
53 except KeyError:
54 CELERY_ON = False
55
56 51
57 52 class ResultWrapper(object):
58 53 def __init__(self, task):
59 54 self.task = task
60 55
61 56 @LazyProperty
62 57 def result(self):
63 58 return self.task
64 59
65 60
66 61 def run_task(task, *args, **kwargs):
67 62 if CELERY_ON:
68 63 try:
69 64 t = task.apply_async(args=args, kwargs=kwargs)
70 log.info('running task %s:%s', t.task_id, task)
65 log.info('running task %s:%s' % (t.task_id, task))
71 66 return t
72 67
73 68 except socket.error, e:
74 69 if isinstance(e, IOError) and e.errno == 111:
75 70 log.debug('Unable to connect to celeryd. Sync execution')
76 71 else:
77 72 log.error(traceback.format_exc())
78 73 except KeyError, e:
79 74 log.debug('Unable to connect to celeryd. Sync execution')
80 75 except Exception, e:
81 76 log.error(traceback.format_exc())
82 77
83 log.debug('executing task %s in sync mode', task)
78 log.debug('executing task %s in sync mode' % task)
84 79 return ResultWrapper(task(*args, **kwargs))
85 80
86 81
87 82 def __get_lockkey(func, *fargs, **fkwargs):
88 83 params = list(fargs)
89 84 params.extend(['%s-%s' % ar for ar in fkwargs.items()])
90 85
91 86 func_name = str(func.__name__) if hasattr(func, '__name__') else str(func)
92 87
93 88 lockkey = 'task_%s.lock' % \
94 89 md5(func_name + '-' + '-'.join(map(safe_str, params))).hexdigest()
95 90 return lockkey
96 91
97 92
98 93 def locked_task(func):
99 94 def __wrapper(func, *fargs, **fkwargs):
100 95 lockkey = __get_lockkey(func, *fargs, **fkwargs)
101 96 lockkey_path = config['here']
102 97
103 log.info('running task with lockkey %s', lockkey)
98 log.info('running task with lockkey %s' % lockkey)
104 99 try:
105 100 l = DaemonLock(file_=jn(lockkey_path, lockkey))
106 101 ret = func(*fargs, **fkwargs)
107 102 l.release()
108 103 return ret
109 104 except LockHeld:
110 105 log.info('LockHeld')
111 106 return 'Task with key %s already running' % lockkey
112 107
113 108 return decorator(__wrapper, func)
114 109
115 110
116 111 def get_session():
117 112 if CELERY_ON:
118 113 engine = engine_from_config(config, 'sqlalchemy.db1.')
119 114 init_model(engine)
120 115 sa = meta.Session
121 116 return sa
122 117
123 118
124 119 def dbsession(func):
125 120 def __wrapper(func, *fargs, **fkwargs):
126 121 try:
127 122 ret = func(*fargs, **fkwargs)
128 123 return ret
129 124 finally:
130 125 if CELERY_ON:
131 126 meta.Session.remove()
132 127
133 128 return decorator(__wrapper, func)
@@ -1,408 +1,414 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.tasks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode task modules, containing all task that suppose to be run
7 7 by celery daemon
8 8
9 9 :created_on: Oct 6, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 from celery.decorators import task
27 27
28 28 import os
29 29 import traceback
30 30 import logging
31 from os.path import dirname as dn, join as jn
31 from os.path import join as jn
32 32
33 33 from time import mktime
34 34 from operator import itemgetter
35 35 from string import lower
36 36
37 37 from pylons import config, url
38 38 from pylons.i18n.translation import _
39 39
40 from rhodecode.lib.vcs import get_backend
41
42 from rhodecode import CELERY_ON
40 43 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
41 from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
42 __get_lockkey, LockHeld, DaemonLock, get_session, dbsession
44 from rhodecode.lib.celerylib import run_task, locked_task, dbsession, \
45 str2bool, __get_lockkey, LockHeld, DaemonLock, get_session
43 46 from rhodecode.lib.helpers import person
44 from rhodecode.lib.smtp_mailer import SmtpMailer
45 from rhodecode.lib.utils import add_cache
47 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
48 from rhodecode.lib.utils import add_cache, action_logger
46 49 from rhodecode.lib.compat import json, OrderedDict
47 50
48 from rhodecode.model.db import RhodeCodeUi, Statistics, Repository, User
51 from rhodecode.model.db import Statistics, Repository, User
49 52
50 from vcs.backends import get_repo
51 from vcs import get_backend
52 53
53 54 add_cache(config)
54 55
55 56 __all__ = ['whoosh_index', 'get_commits_stats',
56 57 'reset_user_password', 'send_email']
57 58
58 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
59
60 59
61 def get_repos_path():
62 sa = get_session()
63 q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
64 return q.ui_value
60 def get_logger(cls):
61 if CELERY_ON:
62 try:
63 log = cls.get_logger()
64 except:
65 log = logging.getLogger(__name__)
66 else:
67 log = logging.getLogger(__name__)
68
69 return log
65 70
66 71
67 72 @task(ignore_result=True)
68 73 @locked_task
69 74 @dbsession
70 75 def whoosh_index(repo_location, full_index):
71 #log = whoosh_index.get_logger()
72 76 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
77 log = whoosh_index.get_logger(whoosh_index)
78 DBS = get_session()
79
73 80 index_location = config['index_dir']
74 81 WhooshIndexingDaemon(index_location=index_location,
75 repo_location=repo_location, sa=get_session())\
82 repo_location=repo_location, sa=DBS)\
76 83 .run(full_index=full_index)
77 84
78 85
79 86 @task(ignore_result=True)
80 87 @dbsession
81 88 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
82 try:
83 log = get_commits_stats.get_logger()
84 except:
85 log = logging.getLogger(__name__)
86
89 log = get_logger(get_commits_stats)
90 DBS = get_session()
87 91 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
88 92 ts_max_y)
89 93 lockkey_path = config['here']
90 94
91 log.info('running task with lockkey %s', lockkey)
95 log.info('running task with lockkey %s' % lockkey)
96
92 97 try:
93 sa = get_session()
94 98 lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
95 99
96 # for js data compatibilty cleans the key for person from '
100 # for js data compatibility cleans the key for person from '
97 101 akc = lambda k: person(k).replace('"', "")
98 102
99 103 co_day_auth_aggr = {}
100 104 commits_by_day_aggregate = {}
101 repos_path = get_repos_path()
102 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
105 repo = Repository.get_by_repo_name(repo_name)
106 if repo is None:
107 return True
108
109 repo = repo.scm_instance
103 110 repo_size = repo.count()
104 111 # return if repo have no revisions
105 112 if repo_size < 1:
106 113 lock.release()
107 114 return True
108 115
109 116 skip_date_limit = True
110 117 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
111 118 last_rev = None
112 119 last_cs = None
113 120 timegetter = itemgetter('time')
114 121
115 dbrepo = sa.query(Repository)\
122 dbrepo = DBS.query(Repository)\
116 123 .filter(Repository.repo_name == repo_name).scalar()
117 cur_stats = sa.query(Statistics)\
124 cur_stats = DBS.query(Statistics)\
118 125 .filter(Statistics.repository == dbrepo).scalar()
119 126
120 127 if cur_stats is not None:
121 128 last_rev = cur_stats.stat_on_revision
122 129
123 130 if last_rev == repo.get_changeset().revision and repo_size > 1:
124 131 # pass silently without any work if we're not on first revision or
125 132 # current state of parsing revision(from db marker) is the
126 133 # last revision
127 134 lock.release()
128 135 return True
129 136
130 137 if cur_stats:
131 138 commits_by_day_aggregate = OrderedDict(json.loads(
132 139 cur_stats.commit_activity_combined))
133 140 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
134 141
135 log.debug('starting parsing %s', parse_limit)
142 log.debug('starting parsing %s' % parse_limit)
136 143 lmktime = mktime
137 144
138 145 last_rev = last_rev + 1 if last_rev >= 0 else 0
139 146 log.debug('Getting revisions from %s to %s' % (
140 147 last_rev, last_rev + parse_limit)
141 148 )
142 149 for cs in repo[last_rev:last_rev + parse_limit]:
143 150 last_cs = cs # remember last parsed changeset
144 151 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
145 152 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
146 153
147 154 if akc(cs.author) in co_day_auth_aggr:
148 155 try:
149 156 l = [timegetter(x) for x in
150 157 co_day_auth_aggr[akc(cs.author)]['data']]
151 158 time_pos = l.index(k)
152 159 except ValueError:
153 160 time_pos = False
154 161
155 162 if time_pos >= 0 and time_pos is not False:
156 163
157 164 datadict = \
158 165 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
159 166
160 167 datadict["commits"] += 1
161 168 datadict["added"] += len(cs.added)
162 169 datadict["changed"] += len(cs.changed)
163 170 datadict["removed"] += len(cs.removed)
164 171
165 172 else:
166 173 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
167 174
168 175 datadict = {"time": k,
169 176 "commits": 1,
170 177 "added": len(cs.added),
171 178 "changed": len(cs.changed),
172 179 "removed": len(cs.removed),
173 180 }
174 181 co_day_auth_aggr[akc(cs.author)]['data']\
175 182 .append(datadict)
176 183
177 184 else:
178 185 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
179 186 co_day_auth_aggr[akc(cs.author)] = {
180 187 "label": akc(cs.author),
181 188 "data": [{"time":k,
182 189 "commits":1,
183 190 "added":len(cs.added),
184 191 "changed":len(cs.changed),
185 192 "removed":len(cs.removed),
186 193 }],
187 194 "schema": ["commits"],
188 195 }
189 196
190 197 #gather all data by day
191 198 if k in commits_by_day_aggregate:
192 199 commits_by_day_aggregate[k] += 1
193 200 else:
194 201 commits_by_day_aggregate[k] = 1
195 202
196 203 overview_data = sorted(commits_by_day_aggregate.items(),
197 204 key=itemgetter(0))
198 205
199 206 if not co_day_auth_aggr:
200 207 co_day_auth_aggr[akc(repo.contact)] = {
201 208 "label": akc(repo.contact),
202 209 "data": [0, 1],
203 210 "schema": ["commits"],
204 211 }
205 212
206 213 stats = cur_stats if cur_stats else Statistics()
207 214 stats.commit_activity = json.dumps(co_day_auth_aggr)
208 215 stats.commit_activity_combined = json.dumps(overview_data)
209 216
210 log.debug('last revison %s', last_rev)
217 log.debug('last revison %s' % last_rev)
211 218 leftovers = len(repo.revisions[last_rev:])
212 log.debug('revisions to parse %s', leftovers)
219 log.debug('revisions to parse %s' % leftovers)
213 220
214 221 if last_rev == 0 or leftovers < parse_limit:
215 222 log.debug('getting code trending stats')
216 223 stats.languages = json.dumps(__get_codes_stats(repo_name))
217 224
218 225 try:
219 226 stats.repository = dbrepo
220 227 stats.stat_on_revision = last_cs.revision if last_cs else 0
221 sa.add(stats)
222 sa.commit()
228 DBS.add(stats)
229 DBS.commit()
223 230 except:
224 231 log.error(traceback.format_exc())
225 sa.rollback()
232 DBS.rollback()
226 233 lock.release()
227 234 return False
228 235
229 # final release
236 #final release
230 237 lock.release()
231 238
232 # execute another task if celery is enabled
239 #execute another task if celery is enabled
233 240 if len(repo.revisions) > 1 and CELERY_ON:
234 241 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y)
235 242 return True
236 243 except LockHeld:
237 244 log.info('LockHeld')
238 245 return 'Task with key %s already running' % lockkey
239 246
240 247 @task(ignore_result=True)
241 248 @dbsession
242 249 def send_password_link(user_email):
243 try:
244 log = reset_user_password.get_logger()
245 except:
246 log = logging.getLogger(__name__)
250 from rhodecode.model.notification import EmailNotificationModel
247 251
248 from rhodecode.lib import auth
252 log = get_logger(send_password_link)
253 DBS = get_session()
249 254
250 255 try:
251 sa = get_session()
252 user = sa.query(User).filter(User.email == user_email).scalar()
253
256 user = User.get_by_email(user_email)
254 257 if user:
258 log.debug('password reset user found %s' % user)
255 259 link = url('reset_password_confirmation', key=user.api_key,
256 260 qualified=True)
257 tmpl = """
258 Hello %s
259
260 We received a request to create a new password for your account.
261
262 You can generate it by clicking following URL:
263
264 %s
265
266 If you didn't request new password please ignore this email.
267 """
261 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
262 body = EmailNotificationModel().get_email_tmpl(reg_type,
263 **{'user':user.short_contact,
264 'reset_url':link})
265 log.debug('sending email')
268 266 run_task(send_email, user_email,
269 "RhodeCode password reset link",
270 tmpl % (user.short_contact, link))
271 log.info('send new password mail to %s', user_email)
272
267 _("password reset link"), body)
268 log.info('send new password mail to %s' % user_email)
269 else:
270 log.debug("password reset email %s not found" % user_email)
273 271 except:
274 log.error('Failed to update user password')
275 272 log.error(traceback.format_exc())
276 273 return False
277 274
278 275 return True
279 276
280 277 @task(ignore_result=True)
281 278 @dbsession
282 279 def reset_user_password(user_email):
283 try:
284 log = reset_user_password.get_logger()
285 except:
286 log = logging.getLogger(__name__)
280 from rhodecode.lib import auth
287 281
288 from rhodecode.lib import auth
282 log = get_logger(reset_user_password)
283 DBS = get_session()
289 284
290 285 try:
291 286 try:
292 sa = get_session()
293 user = sa.query(User).filter(User.email == user_email).scalar()
287 user = User.get_by_email(user_email)
294 288 new_passwd = auth.PasswordGenerator().gen_password(8,
295 289 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
296 290 if user:
297 291 user.password = auth.get_crypt_password(new_passwd)
298 292 user.api_key = auth.generate_api_key(user.username)
299 sa.add(user)
300 sa.commit()
301 log.info('change password for %s', user_email)
293 DBS.add(user)
294 DBS.commit()
295 log.info('change password for %s' % user_email)
302 296 if new_passwd is None:
303 297 raise Exception('unable to generate new password')
304
305 298 except:
306 299 log.error(traceback.format_exc())
307 sa.rollback()
300 DBS.rollback()
308 301
309 302 run_task(send_email, user_email,
310 "Your new RhodeCode password",
303 'Your new password',
311 304 'Your new RhodeCode password:%s' % (new_passwd))
312 log.info('send new password mail to %s', user_email)
305 log.info('send new password mail to %s' % user_email)
313 306
314 307 except:
315 308 log.error('Failed to update user password')
316 309 log.error(traceback.format_exc())
317 310
318 311 return True
319 312
320 313
321 314 @task(ignore_result=True)
322 315 @dbsession
323 def send_email(recipients, subject, body):
316 def send_email(recipients, subject, body, html_body=''):
324 317 """
325 318 Sends an email with defined parameters from the .ini files.
326 319
327 320 :param recipients: list of recipients, it this is empty the defined email
328 321 address from field 'email_to' is used instead
329 322 :param subject: subject of the mail
330 323 :param body: body of the mail
324 :param html_body: html version of body
331 325 """
332 try:
333 log = send_email.get_logger()
334 except:
335 log = logging.getLogger(__name__)
336
337 sa = get_session()
326 log = get_logger(send_email)
327 DBS = get_session()
328
338 329 email_config = config
339
330 subject = "%s %s" % (email_config.get('email_prefix'), subject)
340 331 if not recipients:
341 332 # if recipients are not defined we send to email_config + all admins
342 admins = [
343 u.email for u in sa.query(User).filter(User.admin==True).all()
344 ]
333 admins = [u.email for u in User.query()
334 .filter(User.admin == True).all()]
345 335 recipients = [email_config.get('email_to')] + admins
346 336
347 mail_from = email_config.get('app_email_from')
337 mail_from = email_config.get('app_email_from', 'RhodeCode')
348 338 user = email_config.get('smtp_username')
349 339 passwd = email_config.get('smtp_password')
350 340 mail_server = email_config.get('smtp_server')
351 341 mail_port = email_config.get('smtp_port')
352 342 tls = str2bool(email_config.get('smtp_use_tls'))
353 343 ssl = str2bool(email_config.get('smtp_use_ssl'))
354 344 debug = str2bool(config.get('debug'))
355 345 smtp_auth = email_config.get('smtp_auth')
356 346
357 347 try:
358 m = SmtpMailer(mail_from, user, passwd, mail_server,smtp_auth,
348 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
359 349 mail_port, ssl, tls, debug=debug)
360 m.send(recipients, subject, body)
350 m.send(recipients, subject, body, html_body)
361 351 except:
362 352 log.error('Mail sending failed')
363 353 log.error(traceback.format_exc())
364 354 return False
365 355 return True
366 356
367 357
368 358 @task(ignore_result=True)
369 359 @dbsession
370 360 def create_repo_fork(form_data, cur_user):
361 """
362 Creates a fork of repository using interval VCS methods
363
364 :param form_data:
365 :param cur_user:
366 """
371 367 from rhodecode.model.repo import RepoModel
372 368
373 try:
374 log = create_repo_fork.get_logger()
375 except:
376 log = logging.getLogger(__name__)
369 log = get_logger(create_repo_fork)
370 DBS = get_session()
371
372 base_path = Repository.base_path()
373
374 RepoModel(DBS).create(form_data, cur_user, just_db=True, fork=True)
375
376 alias = form_data['repo_type']
377 org_repo_name = form_data['org_path']
378 fork_name = form_data['repo_name_full']
379 update_after_clone = form_data['update_after_clone']
380 source_repo_path = os.path.join(base_path, org_repo_name)
381 destination_fork_path = os.path.join(base_path, fork_name)
377 382
378 repo_model = RepoModel(get_session())
379 repo_model.create(form_data, cur_user, just_db=True, fork=True)
380 repo_name = form_data['repo_name']
381 repos_path = get_repos_path()
382 repo_path = os.path.join(repos_path, repo_name)
383 repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
384 alias = form_data['repo_type']
383 log.info('creating fork of %s as %s', source_repo_path,
384 destination_fork_path)
385 backend = get_backend(alias)
386 backend(safe_str(destination_fork_path), create=True,
387 src_url=safe_str(source_repo_path),
388 update_after_clone=update_after_clone)
389 action_logger(cur_user, 'user_forked_repo:%s' % fork_name,
390 org_repo_name, '', DBS)
385 391
386 log.info('creating repo fork %s as %s', repo_name, repo_path)
387 backend = get_backend(alias)
388 backend(str(repo_fork_path), create=True, src_url=str(repo_path))
389
392 action_logger(cur_user, 'user_created_fork:%s' % fork_name,
393 fork_name, '', DBS)
394 # finally commit at latest possible stage
395 DBS.commit()
390 396
391 397 def __get_codes_stats(repo_name):
392 repos_path = get_repos_path()
393 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
398 repo = Repository.get_by_repo_name(repo_name).scm_instance
399
394 400 tip = repo.get_changeset()
395 401 code_stats = {}
396 402
397 403 def aggregate(cs):
398 404 for f in cs[2]:
399 405 ext = lower(f.extension)
400 406 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
401 407 if ext in code_stats:
402 408 code_stats[ext] += 1
403 409 else:
404 410 code_stats[ext] = 1
405 411
406 412 map(aggregate, tip.walk('/'))
407 413
408 414 return code_stats or {}
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: 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 renamed from rhodecode/lib/smtp_mailer.py to rhodecode/lib/rcmail/smtp_mailer.py
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 chmod 100755 => 100644
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: 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: 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 renamed from rhodecode/templates/settings/repo_fork.html to rhodecode/templates/forks/fork.html
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: 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 chmod 100755 => 100644, file renamed from rhodecode/tests/test_concurency.py to rhodecode/tests/_test_concurency.py
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: 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
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now