##// END OF EJS Templates
fixed issues with python2.5...
marcink -
r1514:87ec80c2 beta
parent child Browse files
Show More
@@ -0,0 +1,360 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.compat
4 ~~~~~~~~~~~~~~~~~~~~
5
6 Python backward compatibility functions and common libs
7
8
9 :created_on: Oct 7, 2011
10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
13 """
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27 #==============================================================================
28 # json
29 #==============================================================================
30 try:
31 import json
32 except ImportError:
33 import simplejson as json
34
35
36 #==============================================================================
37 # izip_longest
38 #==============================================================================
39 try:
40 from itertools import izip_longest
41 except ImportError:
42 import itertools
43
44 def izip_longest(*args, **kwds): # noqa
45 fillvalue = kwds.get("fillvalue")
46
47 def sentinel(counter=([fillvalue] * (len(args) - 1)).pop):
48 yield counter() # yields the fillvalue, or raises IndexError
49
50 fillers = itertools.repeat(fillvalue)
51 iters = [itertools.chain(it, sentinel(), fillers)
52 for it in args]
53 try:
54 for tup in itertools.izip(*iters):
55 yield tup
56 except IndexError:
57 pass
58
59
60 #==============================================================================
61 # OrderedDict
62 #==============================================================================
63
64 # Python Software Foundation License
65
66 # XXX: it feels like using the class with "is" and "is not" instead of "==" and
67 # "!=" should be faster.
68 class _Nil(object):
69
70 def __repr__(self):
71 return "nil"
72
73 def __eq__(self, other):
74 if (isinstance(other, _Nil)):
75 return True
76 else:
77 return NotImplemented
78
79 def __ne__(self, other):
80 if (isinstance(other, _Nil)):
81 return False
82 else:
83 return NotImplemented
84
85 _nil = _Nil()
86
87 class _odict(object):
88 """Ordered dict data structure, with O(1) complexity for dict operations
89 that modify one element.
90
91 Overwriting values doesn't change their original sequential order.
92 """
93
94 def _dict_impl(self):
95 return None
96
97 def __init__(self, data=(), **kwds):
98 """This doesn't accept keyword initialization as normal dicts to avoid
99 a trap - inside a function or method the keyword args are accessible
100 only as a dict, without a defined order, so their original order is
101 lost.
102 """
103 if kwds:
104 raise TypeError("__init__() of ordered dict takes no keyword "
105 "arguments to avoid an ordering trap.")
106 self._dict_impl().__init__(self)
107 # If you give a normal dict, then the order of elements is undefined
108 if hasattr(data, "iteritems"):
109 for key, val in data.iteritems():
110 self[key] = val
111 else:
112 for key, val in data:
113 self[key] = val
114
115 # Double-linked list header
116 def _get_lh(self):
117 dict_impl = self._dict_impl()
118 if not hasattr(self, '_lh'):
119 dict_impl.__setattr__(self, '_lh', _nil)
120 return dict_impl.__getattribute__(self, '_lh')
121
122 def _set_lh(self, val):
123 self._dict_impl().__setattr__(self, '_lh', val)
124
125 lh = property(_get_lh, _set_lh)
126
127 # Double-linked list tail
128 def _get_lt(self):
129 dict_impl = self._dict_impl()
130 if not hasattr(self, '_lt'):
131 dict_impl.__setattr__(self, '_lt', _nil)
132 return dict_impl.__getattribute__(self, '_lt')
133
134 def _set_lt(self, val):
135 self._dict_impl().__setattr__(self, '_lt', val)
136
137 lt = property(_get_lt, _set_lt)
138
139 def __getitem__(self, key):
140 return self._dict_impl().__getitem__(self, key)[1]
141
142 def __setitem__(self, key, val):
143 dict_impl = self._dict_impl()
144 try:
145 dict_impl.__getitem__(self, key)[1] = val
146 except KeyError, e:
147 new = [dict_impl.__getattribute__(self, 'lt'), val, _nil]
148 dict_impl.__setitem__(self, key, new)
149 if dict_impl.__getattribute__(self, 'lt') == _nil:
150 dict_impl.__setattr__(self, 'lh', key)
151 else:
152 dict_impl.__getitem__(
153 self, dict_impl.__getattribute__(self, 'lt'))[2] = key
154 dict_impl.__setattr__(self, 'lt', key)
155
156 def __delitem__(self, key):
157 dict_impl = self._dict_impl()
158 pred, _ , succ = self._dict_impl().__getitem__(self, key)
159 if pred == _nil:
160 dict_impl.__setattr__(self, 'lh', succ)
161 else:
162 dict_impl.__getitem__(self, pred)[2] = succ
163 if succ == _nil:
164 dict_impl.__setattr__(self, 'lt', pred)
165 else:
166 dict_impl.__getitem__(self, succ)[0] = pred
167 dict_impl.__delitem__(self, key)
168
169 def __contains__(self, key):
170 return key in self.keys()
171
172 def __len__(self):
173 return len(self.keys())
174
175 def __str__(self):
176 pairs = ("%r: %r" % (k, v) for k, v in self.iteritems())
177 return "{%s}" % ", ".join(pairs)
178
179 def __repr__(self):
180 if self:
181 pairs = ("(%r, %r)" % (k, v) for k, v in self.iteritems())
182 return "odict([%s])" % ", ".join(pairs)
183 else:
184 return "odict()"
185
186 def get(self, k, x=None):
187 if k in self:
188 return self._dict_impl().__getitem__(self, k)[1]
189 else:
190 return x
191
192 def __iter__(self):
193 dict_impl = self._dict_impl()
194 curr_key = dict_impl.__getattribute__(self, 'lh')
195 while curr_key != _nil:
196 yield curr_key
197 curr_key = dict_impl.__getitem__(self, curr_key)[2]
198
199 iterkeys = __iter__
200
201 def keys(self):
202 return list(self.iterkeys())
203
204 def itervalues(self):
205 dict_impl = self._dict_impl()
206 curr_key = dict_impl.__getattribute__(self, 'lh')
207 while curr_key != _nil:
208 _, val, curr_key = dict_impl.__getitem__(self, curr_key)
209 yield val
210
211 def values(self):
212 return list(self.itervalues())
213
214 def iteritems(self):
215 dict_impl = self._dict_impl()
216 curr_key = dict_impl.__getattribute__(self, 'lh')
217 while curr_key != _nil:
218 _, val, next_key = dict_impl.__getitem__(self, curr_key)
219 yield curr_key, val
220 curr_key = next_key
221
222 def items(self):
223 return list(self.iteritems())
224
225 def sort(self, cmp=None, key=None, reverse=False):
226 items = [(k, v) for k, v in self.items()]
227 if cmp is not None:
228 items = sorted(items, cmp=cmp)
229 elif key is not None:
230 items = sorted(items, key=key)
231 else:
232 items = sorted(items, key=lambda x: x[1])
233 if reverse:
234 items.reverse()
235 self.clear()
236 self.__init__(items)
237
238 def clear(self):
239 dict_impl = self._dict_impl()
240 dict_impl.clear(self)
241 dict_impl.__setattr__(self, 'lh', _nil)
242 dict_impl.__setattr__(self, 'lt', _nil)
243
244 def copy(self):
245 return self.__class__(self)
246
247 def update(self, data=(), **kwds):
248 if kwds:
249 raise TypeError("update() of ordered dict takes no keyword "
250 "arguments to avoid an ordering trap.")
251 if hasattr(data, "iteritems"):
252 data = data.iteritems()
253 for key, val in data:
254 self[key] = val
255
256 def setdefault(self, k, x=None):
257 try:
258 return self[k]
259 except KeyError:
260 self[k] = x
261 return x
262
263 def pop(self, k, x=_nil):
264 try:
265 val = self[k]
266 del self[k]
267 return val
268 except KeyError:
269 if x == _nil:
270 raise
271 return x
272
273 def popitem(self):
274 try:
275 dict_impl = self._dict_impl()
276 key = dict_impl.__getattribute__(self, 'lt')
277 return key, self.pop(key)
278 except KeyError:
279 raise KeyError("'popitem(): ordered dictionary is empty'")
280
281 def riterkeys(self):
282 """To iterate on keys in reversed order.
283 """
284 dict_impl = self._dict_impl()
285 curr_key = dict_impl.__getattribute__(self, 'lt')
286 while curr_key != _nil:
287 yield curr_key
288 curr_key = dict_impl.__getitem__(self, curr_key)[0]
289
290 __reversed__ = riterkeys
291
292 def rkeys(self):
293 """List of the keys in reversed order.
294 """
295 return list(self.riterkeys())
296
297 def ritervalues(self):
298 """To iterate on values in reversed order.
299 """
300 dict_impl = self._dict_impl()
301 curr_key = dict_impl.__getattribute__(self, 'lt')
302 while curr_key != _nil:
303 curr_key, val, _ = dict_impl.__getitem__(self, curr_key)
304 yield val
305
306 def rvalues(self):
307 """List of the values in reversed order.
308 """
309 return list(self.ritervalues())
310
311 def riteritems(self):
312 """To iterate on (key, value) in reversed order.
313 """
314 dict_impl = self._dict_impl()
315 curr_key = dict_impl.__getattribute__(self, 'lt')
316 while curr_key != _nil:
317 pred_key, val, _ = dict_impl.__getitem__(self, curr_key)
318 yield curr_key, val
319 curr_key = pred_key
320
321 def ritems(self):
322 """List of the (key, value) in reversed order.
323 """
324 return list(self.riteritems())
325
326 def firstkey(self):
327 if self:
328 return self._dict_impl().__getattribute__(self, 'lh')
329 else:
330 raise KeyError("'firstkey(): ordered dictionary is empty'")
331
332 def lastkey(self):
333 if self:
334 return self._dict_impl().__getattribute__(self, 'lt')
335 else:
336 raise KeyError("'lastkey(): ordered dictionary is empty'")
337
338 def as_dict(self):
339 return self._dict_impl()(self.items())
340
341 def _repr(self):
342 """_repr(): low level repr of the whole data contained in the odict.
343 Useful for debugging.
344 """
345 dict_impl = self._dict_impl()
346 form = "odict low level repr lh,lt,data: %r, %r, %s"
347 return form % (dict_impl.__getattribute__(self, 'lh'),
348 dict_impl.__getattribute__(self, 'lt'),
349 dict_impl.__repr__(self))
350
351 class OrderedDict(_odict, dict):
352
353 def _dict_impl(self):
354 return dict
355
356
357 #==============================================================================
358 # OrderedSet
359 #==============================================================================
360 from sqlalchemy.util import OrderedSet
@@ -1,241 +1,242 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 10 :copyright: (C) 2009-2010 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 import json
30 29 import logging
31 30 import types
32 31 import urllib
33 32 import traceback
34 from itertools import izip_longest
33
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 from pylons.controllers.util import Response
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 class JSONRPCError(BaseException):
50 50
51 51 def __init__(self, message):
52 52 self.message = message
53 53
54 54 def __str__(self):
55 55 return str(self.message)
56 56
57 57
58 58 def jsonrpc_error(message, code=None):
59 59 """Generate a Response object with a JSON-RPC error body"""
60 60 return Response(body=json.dumps(dict(result=None,
61 61 error=message)))
62 62
63 63
64 64 class JSONRPCController(WSGIController):
65 65 """
66 66 A WSGI-speaking JSON-RPC controller class
67 67
68 68 See the specification:
69 69 <http://json-rpc.org/wiki/specification>`.
70 70
71 71 Valid controller return values should be json-serializable objects.
72 72
73 73 Sub-classes should catch their exceptions and raise JSONRPCError
74 74 if they want to pass meaningful errors to the client.
75 75
76 76 """
77 77
78 78 def _get_method_args(self):
79 79 """
80 80 Return `self._rpc_args` to dispatched controller method
81 81 chosen by __call__
82 82 """
83 83 return self._rpc_args
84 84
85 85 def __call__(self, environ, start_response):
86 86 """
87 87 Parse the request body as JSON, look up the method on the
88 88 controller and if it exists, dispatch to it.
89 89 """
90 90 if 'CONTENT_LENGTH' not in environ:
91 91 log.debug("No Content-Length")
92 92 return jsonrpc_error(message="No Content-Length in request")
93 93 else:
94 94 length = environ['CONTENT_LENGTH'] or 0
95 95 length = int(environ['CONTENT_LENGTH'])
96 96 log.debug('Content-Length: %s', length)
97 97
98 98 if length == 0:
99 99 log.debug("Content-Length is 0")
100 100 return jsonrpc_error(message="Content-Length is 0")
101 101
102 102 raw_body = environ['wsgi.input'].read(length)
103 103
104 104 try:
105 105 json_body = json.loads(urllib.unquote_plus(raw_body))
106 except ValueError as e:
106 except ValueError, e:
107 107 #catch JSON errors Here
108 108 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
109 109 % (e, urllib.unquote_plus(raw_body)))
110 110
111 111 #check AUTH based on API KEY
112 112 try:
113 113 self._req_api_key = json_body['api_key']
114 114 self._req_method = json_body['method']
115 115 self._req_params = json_body['args']
116 116 log.debug('method: %s, params: %s',
117 117 self._req_method,
118 118 self._req_params)
119 except KeyError as e:
119 except KeyError, e:
120 120 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
121 121
122 122 #check if we can find this session using api_key
123 123 try:
124 124 u = User.get_by_api_key(self._req_api_key)
125 125 auth_u = AuthUser(u.user_id, self._req_api_key)
126 except Exception as e:
126 except Exception, e:
127 127 return jsonrpc_error(message='Invalid API KEY')
128 128
129 129 self._error = None
130 130 try:
131 131 self._func = self._find_method()
132 132 except AttributeError, e:
133 133 return jsonrpc_error(message=str(e))
134 134
135 135 # now that we have a method, add self._req_params to
136 136 # self.kargs and dispatch control to WGIController
137 137 argspec = inspect.getargspec(self._func)
138 138 arglist = argspec[0][1:]
139 139 defaults = argspec[3] or []
140 140 default_empty = types.NotImplementedType
141
142 kwarglist = list(izip_longest(reversed(arglist),reversed(defaults),
141
142 kwarglist = list(izip_longest(reversed(arglist), reversed(defaults),
143 143 fillvalue=default_empty))
144 144
145 145 # this is little trick to inject logged in user for
146 146 # perms decorators to work they expect the controller class to have
147 147 # rhodecode_user attribute set
148 148 self.rhodecode_user = auth_u
149 149
150 150 # This attribute will need to be first param of a method that uses
151 151 # api_key, which is translated to instance of user at that name
152 152 USER_SESSION_ATTR = 'apiuser'
153 153
154 154 if USER_SESSION_ATTR not in arglist:
155 155 return jsonrpc_error(message='This method [%s] does not support '
156 156 'authentication (missing %s param)' %
157 157 (self._func.__name__, USER_SESSION_ATTR))
158 158
159 159 # get our arglist and check if we provided them as args
160 for arg,default in kwarglist:
160 for arg, default in kwarglist:
161 161 if arg == USER_SESSION_ATTR:
162 162 # USER_SESSION_ATTR is something translated from api key and
163 163 # this is checked before so we don't need validate it
164 164 continue
165
165
166 166 # skip the required param check if it's default value is
167 167 # NotImplementedType (default_empty)
168 168 if not self._req_params or (type(default) == default_empty
169 169 and arg not in self._req_params):
170 170 return jsonrpc_error(message=('Missing non optional %s arg '
171 171 'in JSON DATA') % arg)
172 172
173 173 self._rpc_args = {USER_SESSION_ATTR:u}
174 174 self._rpc_args.update(self._req_params)
175 175
176 176 self._rpc_args['action'] = self._req_method
177 177 self._rpc_args['environ'] = environ
178 178 self._rpc_args['start_response'] = start_response
179 179
180 180 status = []
181 181 headers = []
182 182 exc_info = []
183 183 def change_content(new_status, new_headers, new_exc_info=None):
184 184 status.append(new_status)
185 185 headers.extend(new_headers)
186 186 exc_info.append(new_exc_info)
187 187
188 188 output = WSGIController.__call__(self, environ, change_content)
189 189 output = list(output)
190 190 headers.append(('Content-Length', str(len(output[0]))))
191 191 replace_header(headers, 'Content-Type', 'application/json')
192 192 start_response(status[0], headers, exc_info[0])
193 193
194 194 return output
195 195
196 196 def _dispatch_call(self):
197 197 """
198 198 Implement dispatch interface specified by WSGIController
199 199 """
200 200 try:
201 201 raw_response = self._inspect_call(self._func)
202 202 if isinstance(raw_response, HTTPError):
203 203 self._error = str(raw_response)
204 except JSONRPCError as e:
204 except JSONRPCError, e:
205 205 self._error = str(e)
206 except Exception as e:
207 log.error('Encountered unhandled exception: %s' % traceback.format_exc())
206 except Exception, e:
207 log.error('Encountered unhandled exception: %s' \
208 % traceback.format_exc())
208 209 json_exc = JSONRPCError('Internal server error')
209 210 self._error = str(json_exc)
210 211
211 212 if self._error is not None:
212 213 raw_response = None
213 214
214 215 response = dict(result=raw_response, error=self._error)
215 216
216 217 try:
217 218 return json.dumps(response)
218 219 except TypeError, e:
219 220 log.debug('Error encoding response: %s', e)
220 221 return json.dumps(dict(result=None,
221 222 error="Error encoding response"))
222 223
223 224 def _find_method(self):
224 225 """
225 226 Return method named by `self._req_method` in controller if able
226 227 """
227 228 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
228 229 if self._req_method.startswith('_'):
229 230 raise AttributeError("Method not allowed")
230 231
231 232 try:
232 233 func = getattr(self, self._req_method, None)
233 234 except UnicodeEncodeError:
234 235 raise AttributeError("Problem decoding unicode in requested "
235 236 "method name.")
236 237
237 238 if isinstance(func, types.MethodType):
238 239 return func
239 240 else:
240 241 raise AttributeError("No such method: %s" % self._req_method)
241 242
@@ -1,78 +1,78 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 10 :copyright: (C) 2009-2011 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 from rhodecode.lib.odict import OrderedDict
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 49
50 50 bt = {}
51 51 bt_closed = {}
52 52
53 53 for bn, heads in localrepo.branchmap().iteritems():
54 54 tip = heads[-1]
55 55 if 'close' not in localrepo.changelog.read(tip)[5]:
56 56 bt[bn] = tip
57 57 else:
58 58 bt_closed[bn] = tip
59 59 return bt, bt_closed
60 60
61 61
62 62 bt, bt_closed = _branchtags(c.rhodecode_repo._repo)
63 63 cs_g = c.rhodecode_repo.get_changeset
64 64 _branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in
65 65 bt.items()]
66 66
67 67 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in
68 68 bt_closed.items()]
69 69
70 70 c.repo_branches = OrderedDict(sorted(_branches,
71 71 key=lambda ctx: ctx[0],
72 72 reverse=False))
73 73 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
74 74 key=lambda ctx: ctx[0],
75 75 reverse=False))
76 76
77 77
78 78 return render('branches/branches.html')
@@ -1,121 +1,116 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 10 :copyright: (C) 2009-2011 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 try:
29 import json
30 except ImportError:
31 #python 2.5 compatibility
32 import simplejson as json
33
34 28 from mercurial import graphmod
35 29 from pylons import request, session, tmpl_context as c
36 30
37 31 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 32 from rhodecode.lib.base import BaseRepoController, render
39 33 from rhodecode.lib.helpers import RepoPage
34 from rhodecode.lib.compat import json
40 35
41 36 log = logging.getLogger(__name__)
42 37
43 38
44 39 class ChangelogController(BaseRepoController):
45 40
46 41 @LoginRequired()
47 42 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 43 'repository.admin')
49 44 def __before__(self):
50 45 super(ChangelogController, self).__before__()
51 46 c.affected_files_cut_off = 60
52 47
53 48 def index(self):
54 49 limit = 100
55 50 default = 20
56 51 if request.params.get('size'):
57 52 try:
58 53 int_size = int(request.params.get('size'))
59 54 except ValueError:
60 55 int_size = default
61 56 int_size = int_size if int_size <= limit else limit
62 57 c.size = int_size
63 58 session['changelog_size'] = c.size
64 59 session.save()
65 60 else:
66 61 c.size = int(session.get('changelog_size', default))
67 62
68 63 p = int(request.params.get('page', 1))
69 64 branch_name = request.params.get('branch', None)
70 65 c.total_cs = len(c.rhodecode_repo)
71 66 c.pagination = RepoPage(c.rhodecode_repo, page=p,
72 67 item_count=c.total_cs, items_per_page=c.size,
73 68 branch_name=branch_name)
74 69
75 70 self._graph(c.rhodecode_repo, c.total_cs, c.size, p)
76 71
77 72 return render('changelog/changelog.html')
78 73
79 74 def changelog_details(self, cs):
80 75 if request.environ.get('HTTP_X_PARTIAL_XHR'):
81 76 c.cs = c.rhodecode_repo.get_changeset(cs)
82 77 return render('changelog/changelog_details.html')
83 78
84 79 def _graph(self, repo, repo_size, size, p):
85 80 """
86 81 Generates a DAG graph for mercurial
87 82
88 83 :param repo: repo instance
89 84 :param size: number of commits to show
90 85 :param p: page number
91 86 """
92 87 if not repo.revisions:
93 88 c.jsdata = json.dumps([])
94 89 return
95 90
96 91 revcount = min(repo_size, size)
97 92 offset = 1 if p == 1 else ((p - 1) * revcount + 1)
98 93 try:
99 94 rev_end = repo.revisions.index(repo.revisions[(-1 * offset)])
100 95 except IndexError:
101 96 rev_end = repo.revisions.index(repo.revisions[-1])
102 97 rev_start = max(0, rev_end - revcount)
103 98
104 99 data = []
105 100 rev_end += 1
106 101
107 102 if repo.alias == 'git':
108 103 for _ in xrange(rev_start, rev_end):
109 104 vtx = [0, 1]
110 105 edges = [[0, 0, 1]]
111 106 data.append(['', vtx, edges])
112 107
113 108 elif repo.alias == 'hg':
114 109 revs = list(reversed(xrange(rev_start, rev_end)))
115 110 c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs))
116 111 for (id, type, ctx, vtx, edges) in c.dag:
117 112 if type != graphmod.CHANGESET:
118 113 continue
119 114 data.append(['', vtx, edges])
120 115
121 116 c.jsdata = json.dumps(data)
@@ -1,252 +1,252 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 11 :copyright: (C) 2009-2011 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 28
29 29 from pylons import tmpl_context as c, url, request, response
30 30 from pylons.i18n.translation import _
31 31 from pylons.controllers.util import redirect
32 32
33 33 import rhodecode.lib.helpers as h
34 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.utils import EmptyChangeset
37 from rhodecode.lib.odict import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 38
39 39 from vcs.exceptions import RepositoryError, ChangesetError, \
40 40 ChangesetDoesNotExistError
41 41 from vcs.nodes import FileNode
42 42 from vcs.utils import diffs as differ
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class ChangesetController(BaseRepoController):
48 48
49 49 @LoginRequired()
50 50 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
51 51 'repository.admin')
52 52 def __before__(self):
53 53 super(ChangesetController, self).__before__()
54 54 c.affected_files_cut_off = 60
55 55
56 56 def index(self, revision):
57 57
58 58 def wrap_to_table(str):
59 59
60 60 return '''<table class="code-difftable">
61 61 <tr class="line">
62 62 <td class="lineno new"></td>
63 63 <td class="code"><pre>%s</pre></td>
64 64 </tr>
65 65 </table>''' % str
66 66
67 67 #get ranges of revisions if preset
68 68 rev_range = revision.split('...')[:2]
69 69
70 70 try:
71 71 if len(rev_range) == 2:
72 72 rev_start = rev_range[0]
73 73 rev_end = rev_range[1]
74 74 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
75 75 end=rev_end)
76 76 else:
77 77 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
78 78
79 79 c.cs_ranges = list(rev_ranges)
80 80
81 81 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
82 82 log.error(traceback.format_exc())
83 83 h.flash(str(e), category='warning')
84 84 return redirect(url('home'))
85 85
86 86 c.changes = OrderedDict()
87 87 c.sum_added = 0
88 88 c.sum_removed = 0
89 89 c.lines_added = 0
90 90 c.lines_deleted = 0
91 91 c.cut_off = False # defines if cut off limit is reached
92 92
93 93 # Iterate over ranges (default changeset view is always one changeset)
94 94 for changeset in c.cs_ranges:
95 95 c.changes[changeset.raw_id] = []
96 96 try:
97 97 changeset_parent = changeset.parents[0]
98 98 except IndexError:
99 99 changeset_parent = None
100 100
101 101 #==================================================================
102 102 # ADDED FILES
103 103 #==================================================================
104 104 for node in changeset.added:
105 105
106 106 filenode_old = FileNode(node.path, '', EmptyChangeset())
107 107 if filenode_old.is_binary or node.is_binary:
108 108 diff = wrap_to_table(_('binary file'))
109 109 st = (0, 0)
110 110 else:
111 111 # in this case node.size is good parameter since those are
112 112 # added nodes and their size defines how many changes were
113 113 # made
114 114 c.sum_added += node.size
115 115 if c.sum_added < self.cut_off_limit:
116 116 f_gitdiff = differ.get_gitdiff(filenode_old, node)
117 117 d = differ.DiffProcessor(f_gitdiff, format='gitdiff')
118 118
119 119 st = d.stat()
120 120 diff = d.as_html()
121 121
122 122 else:
123 123 diff = wrap_to_table(_('Changeset is to big and '
124 124 'was cut off, see raw '
125 125 'changeset instead'))
126 126 c.cut_off = True
127 127 break
128 128
129 129 cs1 = None
130 130 cs2 = node.last_changeset.raw_id
131 131 c.lines_added += st[0]
132 132 c.lines_deleted += st[1]
133 133 c.changes[changeset.raw_id].append(('added', node, diff,
134 134 cs1, cs2, st))
135 135
136 136 #==================================================================
137 137 # CHANGED FILES
138 138 #==================================================================
139 139 if not c.cut_off:
140 140 for node in changeset.changed:
141 141 try:
142 142 filenode_old = changeset_parent.get_node(node.path)
143 143 except ChangesetError:
144 144 log.warning('Unable to fetch parent node for diff')
145 145 filenode_old = FileNode(node.path, '',
146 146 EmptyChangeset())
147 147
148 148 if filenode_old.is_binary or node.is_binary:
149 149 diff = wrap_to_table(_('binary file'))
150 150 st = (0, 0)
151 151 else:
152 152
153 153 if c.sum_removed < self.cut_off_limit:
154 154 f_gitdiff = differ.get_gitdiff(filenode_old, node)
155 155 d = differ.DiffProcessor(f_gitdiff,
156 156 format='gitdiff')
157 157 st = d.stat()
158 158 if (st[0] + st[1]) * 256 > self.cut_off_limit:
159 159 diff = wrap_to_table(_('Diff is to big '
160 160 'and was cut off, see '
161 161 'raw diff instead'))
162 162 else:
163 163 diff = d.as_html()
164 164
165 165 if diff:
166 166 c.sum_removed += len(diff)
167 167 else:
168 168 diff = wrap_to_table(_('Changeset is to big and '
169 169 'was cut off, see raw '
170 170 'changeset instead'))
171 171 c.cut_off = True
172 172 break
173 173
174 174 cs1 = filenode_old.last_changeset.raw_id
175 175 cs2 = node.last_changeset.raw_id
176 176 c.lines_added += st[0]
177 177 c.lines_deleted += st[1]
178 178 c.changes[changeset.raw_id].append(('changed', node, diff,
179 179 cs1, cs2, st))
180 180
181 181 #==================================================================
182 182 # REMOVED FILES
183 183 #==================================================================
184 184 if not c.cut_off:
185 185 for node in changeset.removed:
186 186 c.changes[changeset.raw_id].append(('removed', node, None,
187 187 None, None, (0, 0)))
188 188
189 189 if len(c.cs_ranges) == 1:
190 190 c.changeset = c.cs_ranges[0]
191 191 c.changes = c.changes[c.changeset.raw_id]
192 192
193 193 return render('changeset/changeset.html')
194 194 else:
195 195 return render('changeset/changeset_range.html')
196 196
197 197 def raw_changeset(self, revision):
198 198
199 199 method = request.GET.get('diff', 'show')
200 200 try:
201 201 c.scm_type = c.rhodecode_repo.alias
202 202 c.changeset = c.rhodecode_repo.get_changeset(revision)
203 203 except RepositoryError:
204 204 log.error(traceback.format_exc())
205 205 return redirect(url('home'))
206 206 else:
207 207 try:
208 208 c.changeset_parent = c.changeset.parents[0]
209 209 except IndexError:
210 210 c.changeset_parent = None
211 211 c.changes = []
212 212
213 213 for node in c.changeset.added:
214 214 filenode_old = FileNode(node.path, '')
215 215 if filenode_old.is_binary or node.is_binary:
216 216 diff = _('binary file') + '\n'
217 217 else:
218 218 f_gitdiff = differ.get_gitdiff(filenode_old, node)
219 219 diff = differ.DiffProcessor(f_gitdiff,
220 220 format='gitdiff').raw_diff()
221 221
222 222 cs1 = None
223 223 cs2 = node.last_changeset.raw_id
224 224 c.changes.append(('added', node, diff, cs1, cs2))
225 225
226 226 for node in c.changeset.changed:
227 227 filenode_old = c.changeset_parent.get_node(node.path)
228 228 if filenode_old.is_binary or node.is_binary:
229 229 diff = _('binary file')
230 230 else:
231 231 f_gitdiff = differ.get_gitdiff(filenode_old, node)
232 232 diff = differ.DiffProcessor(f_gitdiff,
233 233 format='gitdiff').raw_diff()
234 234
235 235 cs1 = filenode_old.last_changeset.raw_id
236 236 cs2 = node.last_changeset.raw_id
237 237 c.changes.append(('changed', node, diff, cs1, cs2))
238 238
239 239 response.content_type = 'text/plain'
240 240
241 241 if method == 'download':
242 242 response.content_disposition = 'attachment; filename=%s.patch' \
243 243 % revision
244 244
245 245 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
246 246 c.changeset.parents])
247 247
248 248 c.diffs = ''
249 249 for x in c.changes:
250 250 c.diffs += x[2]
251 251
252 252 return render('changeset/raw_changeset.html')
@@ -1,507 +1,514 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) 2009-2011 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 30 from os.path import join as jn
31 31
32 32 from pylons import request, response, session, tmpl_context as c, url
33 33 from pylons.i18n.translation import _
34 34 from pylons.controllers.util import redirect
35 35 from pylons.decorators import jsonify
36 36
37 37 from vcs.conf import settings
38 38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
39 39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
40 40 from vcs.nodes import FileNode, NodeKind
41 41 from vcs.utils import diffs as differ
42 42
43 43 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
44 44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 45 from rhodecode.lib.base import BaseRepoController, render
46 46 from rhodecode.lib.utils import EmptyChangeset
47 47 import rhodecode.lib.helpers as h
48 48 from rhodecode.model.repo import RepoModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class FilesController(BaseRepoController):
54 54
55 55 @LoginRequired()
56 56 def __before__(self):
57 57 super(FilesController, self).__before__()
58 58 c.cut_off_limit = self.cut_off_limit
59 59
60 60 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
61 61 """
62 62 Safe way to get changeset if error occur it redirects to tip with
63 63 proper message
64 64
65 65 :param rev: revision to fetch
66 66 :param repo_name: repo name to redirect after
67 67 """
68 68
69 69 try:
70 70 return c.rhodecode_repo.get_changeset(rev)
71 71 except EmptyRepositoryError, e:
72 72 if not redirect_after:
73 73 return None
74 74 url_ = url('files_add_home',
75 75 repo_name=c.repo_name,
76 76 revision=0, f_path='')
77 77 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
78 78 h.flash(h.literal(_('There are no files yet %s' % add_new)),
79 79 category='warning')
80 80 redirect(h.url('summary_home', repo_name=repo_name))
81 81
82 82 except RepositoryError, e:
83 83 h.flash(str(e), category='warning')
84 84 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
85 85
86 86 def __get_filenode_or_redirect(self, repo_name, cs, path):
87 87 """
88 88 Returns file_node, if error occurs or given path is directory,
89 89 it'll redirect to top level path
90 90
91 91 :param repo_name: repo_name
92 92 :param cs: given changeset
93 93 :param path: path to lookup
94 94 """
95 95
96 96 try:
97 97 file_node = cs.get_node(path)
98 98 if file_node.is_dir():
99 99 raise RepositoryError('given path is a directory')
100 100 except RepositoryError, e:
101 101 h.flash(str(e), category='warning')
102 102 redirect(h.url('files_home', repo_name=repo_name,
103 103 revision=cs.raw_id))
104 104
105 105 return file_node
106 106
107 107
108 108 def __get_paths(self, changeset, starting_path):
109 109 """recursive walk in root dir and return a set of all path in that dir
110 110 based on repository walk function
111 111 """
112 112 _files = list()
113 113 _dirs = list()
114 114
115 115 try:
116 116 tip = changeset
117 117 for topnode, dirs, files in tip.walk(starting_path):
118 118 for f in files:
119 119 _files.append(f.path)
120 120 for d in dirs:
121 121 _dirs.append(d.path)
122 122 except RepositoryError, e:
123 123 log.debug(traceback.format_exc())
124 124 pass
125 125 return _dirs, _files
126 126
127 127 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
128 128 'repository.admin')
129 129 def index(self, repo_name, revision, f_path):
130 130 #reditect to given revision from form if given
131 131 post_revision = request.POST.get('at_rev', None)
132 132 if post_revision:
133 133 cs = self.__get_cs_or_redirect(post_revision, repo_name)
134 134 redirect(url('files_home', repo_name=c.repo_name,
135 135 revision=cs.raw_id, f_path=f_path))
136 136
137 137 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
138 138 c.branch = request.GET.get('branch', None)
139 139 c.f_path = f_path
140 140
141 141 cur_rev = c.changeset.revision
142 142
143 143 #prev link
144 144 try:
145 145 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
146 146 c.url_prev = url('files_home', repo_name=c.repo_name,
147 147 revision=prev_rev.raw_id, f_path=f_path)
148 148 if c.branch:
149 149 c.url_prev += '?branch=%s' % c.branch
150 150 except (ChangesetDoesNotExistError, VCSError):
151 151 c.url_prev = '#'
152 152
153 153 #next link
154 154 try:
155 155 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
156 156 c.url_next = url('files_home', repo_name=c.repo_name,
157 157 revision=next_rev.raw_id, f_path=f_path)
158 158 if c.branch:
159 159 c.url_next += '?branch=%s' % c.branch
160 160 except (ChangesetDoesNotExistError, VCSError):
161 161 c.url_next = '#'
162 162
163 163 #files or dirs
164 164 try:
165 165 c.files_list = c.changeset.get_node(f_path)
166 166
167 167 if c.files_list.is_file():
168 168 c.file_history = self._get_node_history(c.changeset, f_path)
169 169 else:
170 170 c.file_history = []
171 171 except RepositoryError, e:
172 172 h.flash(str(e), category='warning')
173 173 redirect(h.url('files_home', repo_name=repo_name,
174 174 revision=revision))
175 175
176 176 return render('files/files.html')
177 177
178 178 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
179 179 'repository.admin')
180 180 def rawfile(self, repo_name, revision, f_path):
181 181 cs = self.__get_cs_or_redirect(revision, repo_name)
182 182 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
183 183
184 184 response.content_disposition = 'attachment; filename=%s' % \
185 185 safe_str(f_path.split(os.sep)[-1])
186 186
187 187 response.content_type = file_node.mimetype
188 188 return file_node.content
189 189
190 190 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
191 191 'repository.admin')
192 192 def raw(self, repo_name, revision, f_path):
193 193 cs = self.__get_cs_or_redirect(revision, repo_name)
194 194 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
195 195
196 196 raw_mimetype_mapping = {
197 197 # map original mimetype to a mimetype used for "show as raw"
198 198 # you can also provide a content-disposition to override the
199 199 # default "attachment" disposition.
200 200 # orig_type: (new_type, new_dispo)
201 201
202 202 # show images inline:
203 203 'image/x-icon': ('image/x-icon', 'inline'),
204 204 'image/png': ('image/png', 'inline'),
205 205 'image/gif': ('image/gif', 'inline'),
206 206 'image/jpeg': ('image/jpeg', 'inline'),
207 207 'image/svg+xml': ('image/svg+xml', 'inline'),
208 208 }
209 209
210 210 mimetype = file_node.mimetype
211 211 try:
212 212 mimetype, dispo = raw_mimetype_mapping[mimetype]
213 213 except KeyError:
214 214 # we don't know anything special about this, handle it safely
215 215 if file_node.is_binary:
216 216 # do same as download raw for binary files
217 217 mimetype, dispo = 'application/octet-stream', 'attachment'
218 218 else:
219 219 # do not just use the original mimetype, but force text/plain,
220 220 # otherwise it would serve text/html and that might be unsafe.
221 221 # Note: underlying vcs library fakes text/plain mimetype if the
222 222 # mimetype can not be determined and it thinks it is not
223 223 # binary.This might lead to erroneous text display in some
224 224 # cases, but helps in other cases, like with text files
225 225 # without extension.
226 226 mimetype, dispo = 'text/plain', 'inline'
227 227
228 228 if dispo == 'attachment':
229 229 dispo = 'attachment; filename=%s' % \
230 230 safe_str(f_path.split(os.sep)[-1])
231 231
232 232 response.content_disposition = dispo
233 233 response.content_type = mimetype
234 234 return file_node.content
235 235
236 236 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
237 237 'repository.admin')
238 238 def annotate(self, repo_name, revision, f_path):
239 239 c.cs = self.__get_cs_or_redirect(revision, repo_name)
240 240 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
241 241
242 242 c.file_history = self._get_node_history(c.cs, f_path)
243 243 c.f_path = f_path
244 244 return render('files/files_annotate.html')
245 245
246 246 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
247 247 def edit(self, repo_name, revision, f_path):
248 248 r_post = request.POST
249 249
250 250 c.cs = self.__get_cs_or_redirect(revision, repo_name)
251 251 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
252 252
253 253 if c.file.is_binary:
254 254 return redirect(url('files_home', repo_name=c.repo_name,
255 255 revision=c.cs.raw_id, f_path=f_path))
256 256
257 257 c.f_path = f_path
258 258
259 259 if r_post:
260 260
261 261 old_content = c.file.content
262 262 sl = old_content.splitlines(1)
263 263 first_line = sl[0] if sl else ''
264 264 # modes: 0 - Unix, 1 - Mac, 2 - DOS
265 265 mode = detect_mode(first_line, 0)
266 266 content = convert_line_endings(r_post.get('content'), mode)
267 267
268 268 message = r_post.get('message') or (_('Edited %s via RhodeCode')
269 269 % (f_path))
270 270 author = self.rhodecode_user.full_contact
271 271
272 272 if content == old_content:
273 273 h.flash(_('No changes'),
274 274 category='warning')
275 275 return redirect(url('changeset_home', repo_name=c.repo_name,
276 276 revision='tip'))
277 277
278 278 try:
279 279 self.scm_model.commit_change(repo=c.rhodecode_repo,
280 280 repo_name=repo_name, cs=c.cs,
281 281 user=self.rhodecode_user,
282 282 author=author, message=message,
283 283 content=content, f_path=f_path)
284 284 h.flash(_('Successfully committed to %s' % f_path),
285 285 category='success')
286 286
287 287 except Exception:
288 288 log.error(traceback.format_exc())
289 289 h.flash(_('Error occurred during commit'), category='error')
290 290 return redirect(url('changeset_home',
291 291 repo_name=c.repo_name, revision='tip'))
292 292
293 293 return render('files/files_edit.html')
294 294
295 295 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
296 296 def add(self, repo_name, revision, f_path):
297 297 r_post = request.POST
298 298 c.cs = self.__get_cs_or_redirect(revision, repo_name,
299 299 redirect_after=False)
300 300 if c.cs is None:
301 301 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
302 302
303 303 c.f_path = f_path
304 304
305 305 if r_post:
306 306 unix_mode = 0
307 307 content = convert_line_endings(r_post.get('content'), unix_mode)
308 308
309 309 message = r_post.get('message') or (_('Added %s via RhodeCode')
310 310 % (f_path))
311 311 location = r_post.get('location')
312 312 filename = r_post.get('filename')
313 313 file_obj = r_post.get('upload_file', None)
314 314
315 315 if file_obj is not None and hasattr(file_obj, 'filename'):
316 316 filename = file_obj.filename
317 317 content = file_obj.file
318 318
319 #TODO: REMOVE THIS !!
320 ################################
321 import ipdb;ipdb.set_trace()
322 print 'setting ipdb debuggin for rhodecode.controllers.files.FilesController.add'
323 ################################
324
325
319 326 node_path = os.path.join(location, filename)
320 327 author = self.rhodecode_user.full_contact
321 328
322 329 if not content:
323 330 h.flash(_('No content'), category='warning')
324 331 return redirect(url('changeset_home', repo_name=c.repo_name,
325 332 revision='tip'))
326 333 if not filename:
327 334 h.flash(_('No filename'), category='warning')
328 335 return redirect(url('changeset_home', repo_name=c.repo_name,
329 336 revision='tip'))
330 337
331 338 try:
332 339 self.scm_model.create_node(repo=c.rhodecode_repo,
333 340 repo_name=repo_name, cs=c.cs,
334 341 user=self.rhodecode_user,
335 342 author=author, message=message,
336 343 content=content, f_path=node_path)
337 344 h.flash(_('Successfully committed to %s' % node_path),
338 345 category='success')
339 346 except NodeAlreadyExistsError, e:
340 347 h.flash(_(e), category='error')
341 348 except Exception:
342 349 log.error(traceback.format_exc())
343 350 h.flash(_('Error occurred during commit'), category='error')
344 351 return redirect(url('changeset_home',
345 352 repo_name=c.repo_name, revision='tip'))
346 353
347 354 return render('files/files_add.html')
348 355
349 356 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
350 357 'repository.admin')
351 358 def archivefile(self, repo_name, fname):
352 359
353 360 fileformat = None
354 361 revision = None
355 362 ext = None
356 363 subrepos = request.GET.get('subrepos') == 'true'
357 364
358 365 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
359 366 archive_spec = fname.split(ext_data[1])
360 367 if len(archive_spec) == 2 and archive_spec[1] == '':
361 368 fileformat = a_type or ext_data[1]
362 369 revision = archive_spec[0]
363 370 ext = ext_data[1]
364 371
365 372 try:
366 373 dbrepo = RepoModel().get_by_repo_name(repo_name)
367 374 if dbrepo.enable_downloads is False:
368 375 return _('downloads disabled')
369 376
370 377 cs = c.rhodecode_repo.get_changeset(revision)
371 378 content_type = settings.ARCHIVE_SPECS[fileformat][0]
372 379 except ChangesetDoesNotExistError:
373 380 return _('Unknown revision %s') % revision
374 381 except EmptyRepositoryError:
375 382 return _('Empty repository')
376 383 except (ImproperArchiveTypeError, KeyError):
377 384 return _('Unknown archive type')
378 385
379 386 response.content_type = content_type
380 387 response.content_disposition = 'attachment; filename=%s-%s%s' \
381 388 % (repo_name, revision, ext)
382 389
383 390 import tempfile
384 391 archive = tempfile.mkstemp()[1]
385 392 t = open(archive, 'wb')
386 393 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
387 394
388 395 def get_chunked_archive(archive):
389 396 stream = open(archive, 'rb')
390 397 while True:
391 398 data = stream.read(4096)
392 399 if not data:
393 400 os.remove(archive)
394 401 break
395 402 yield data
396 403
397 404 return get_chunked_archive(archive)
398 405
399 406 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
400 407 'repository.admin')
401 408 def diff(self, repo_name, f_path):
402 409 diff1 = request.GET.get('diff1')
403 410 diff2 = request.GET.get('diff2')
404 411 c.action = request.GET.get('diff')
405 412 c.no_changes = diff1 == diff2
406 413 c.f_path = f_path
407 414 c.big_diff = False
408 415
409 416 try:
410 417 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
411 418 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
412 419 node1 = c.changeset_1.get_node(f_path)
413 420 else:
414 421 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
415 422 node1 = FileNode('.', '', changeset=c.changeset_1)
416 423
417 424 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
418 425 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
419 426 node2 = c.changeset_2.get_node(f_path)
420 427 else:
421 428 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
422 429 node2 = FileNode('.', '', changeset=c.changeset_2)
423 430 except RepositoryError:
424 431 return redirect(url('files_home',
425 432 repo_name=c.repo_name, f_path=f_path))
426 433
427 434 if c.action == 'download':
428 435 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
429 436 format='gitdiff')
430 437
431 438 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
432 439 response.content_type = 'text/plain'
433 440 response.content_disposition = 'attachment; filename=%s' \
434 441 % diff_name
435 442 return diff.raw_diff()
436 443
437 444 elif c.action == 'raw':
438 445 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
439 446 format='gitdiff')
440 447 response.content_type = 'text/plain'
441 448 return diff.raw_diff()
442 449
443 450 elif c.action == 'diff':
444 451 if node1.is_binary or node2.is_binary:
445 452 c.cur_diff = _('Binary file')
446 453 elif node1.size > self.cut_off_limit or \
447 454 node2.size > self.cut_off_limit:
448 455 c.cur_diff = ''
449 456 c.big_diff = True
450 457 else:
451 458 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
452 459 format='gitdiff')
453 460 c.cur_diff = diff.as_html()
454 461 else:
455 462
456 463 #default option
457 464 if node1.is_binary or node2.is_binary:
458 465 c.cur_diff = _('Binary file')
459 466 elif node1.size > self.cut_off_limit or \
460 467 node2.size > self.cut_off_limit:
461 468 c.cur_diff = ''
462 469 c.big_diff = True
463 470
464 471 else:
465 472 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
466 473 format='gitdiff')
467 474 c.cur_diff = diff.as_html()
468 475
469 476 if not c.cur_diff and not c.big_diff:
470 477 c.no_changes = True
471 478 return render('files/file_diff.html')
472 479
473 480 def _get_node_history(self, cs, f_path):
474 481 changesets = cs.get_file_history(f_path)
475 482 hist_l = []
476 483
477 484 changesets_group = ([], _("Changesets"))
478 485 branches_group = ([], _("Branches"))
479 486 tags_group = ([], _("Tags"))
480 487
481 488 for chs in changesets:
482 489 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
483 490 changesets_group[0].append((chs.raw_id, n_desc,))
484 491
485 492 hist_l.append(changesets_group)
486 493
487 494 for name, chs in c.rhodecode_repo.branches.items():
488 495 #chs = chs.split(':')[-1]
489 496 branches_group[0].append((chs, name),)
490 497 hist_l.append(branches_group)
491 498
492 499 for name, chs in c.rhodecode_repo.tags.items():
493 500 #chs = chs.split(':')[-1]
494 501 tags_group[0].append((chs, name),)
495 502 hist_l.append(tags_group)
496 503
497 504 return hist_l
498 505
499 506 @jsonify
500 507 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
501 508 'repository.admin')
502 509 def nodelist(self, repo_name, revision, f_path):
503 510 if request.environ.get('HTTP_X_PARTIAL_XHR'):
504 511 cs = self.__get_cs_or_redirect(revision, repo_name)
505 512 _d, _f = self.__get_paths(cs, f_path)
506 513 return _d + _f
507 514
@@ -1,189 +1,183 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 10 :copyright: (C) 2009-2011 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 calendar
27 27 import logging
28 28 from time import mktime
29 29 from datetime import datetime, timedelta, date
30 30
31 31 from vcs.exceptions import ChangesetError
32 32
33 33 from pylons import tmpl_context as c, request, url
34 34 from pylons.i18n.translation import _
35 35
36 36 from rhodecode.model.db import Statistics, Repository
37 37 from rhodecode.model.repo import RepoModel
38 38
39 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
40 40 from rhodecode.lib.base import BaseRepoController, render
41 41 from rhodecode.lib.utils import EmptyChangeset
42 from rhodecode.lib.odict import OrderedDict
43 42
44 43 from rhodecode.lib.celerylib import run_task
45 44 from rhodecode.lib.celerylib.tasks import get_commits_stats, \
46 45 LANGUAGES_EXTENSIONS_MAP
47 46 from rhodecode.lib.helpers import RepoPage
47 from rhodecode.lib.compat import json, OrderedDict
48 48
49 try:
50 import json
51 except ImportError:
52 #python 2.5 compatibility
53 import simplejson as json
54
55 49 log = logging.getLogger(__name__)
56 50
57 51
58 52 class SummaryController(BaseRepoController):
59 53
60 54 @LoginRequired()
61 55 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
62 56 'repository.admin')
63 57 def __before__(self):
64 58 super(SummaryController, self).__before__()
65 59
66 60 def index(self, repo_name):
67 61
68 62 e = request.environ
69 63 c.dbrepo = dbrepo = c.rhodecode_db_repo
70 64
71 65 c.following = self.scm_model.is_following_repo(repo_name,
72 66 self.rhodecode_user.user_id)
73 67
74 68 def url_generator(**kw):
75 69 return url('shortlog_home', repo_name=repo_name, size=10, **kw)
76 70
77 71 c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
78 72 items_per_page=10, url=url_generator)
79 73
80 74 if self.rhodecode_user.username == 'default':
81 75 #for default(anonymous) user we don't need to pass credentials
82 76 username = ''
83 77 password = ''
84 78 else:
85 79 username = str(self.rhodecode_user.username)
86 80 password = '@'
87 81
88 82 if e.get('wsgi.url_scheme') == 'https':
89 83 split_s = 'https://'
90 84 else:
91 85 split_s = 'http://'
92 86
93 87 qualified_uri = [split_s] + [url.current(qualified=True)\
94 88 .split(split_s)[-1]]
95 89 uri = u'%(proto)s%(user)s%(pass)s%(rest)s' \
96 90 % {'user': username,
97 91 'pass': password,
98 92 'proto': qualified_uri[0],
99 93 'rest': qualified_uri[1]}
100 94 c.clone_repo_url = uri
101 95 c.repo_tags = OrderedDict()
102 96 for name, hash in c.rhodecode_repo.tags.items()[:10]:
103 97 try:
104 98 c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash)
105 99 except ChangesetError:
106 100 c.repo_tags[name] = EmptyChangeset(hash)
107 101
108 102 c.repo_branches = OrderedDict()
109 103 for name, hash in c.rhodecode_repo.branches.items()[:10]:
110 104 try:
111 105 c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash)
112 106 except ChangesetError:
113 107 c.repo_branches[name] = EmptyChangeset(hash)
114 108
115 109 td = date.today() + timedelta(days=1)
116 110 td_1m = td - timedelta(days=calendar.mdays[td.month])
117 111 td_1y = td - timedelta(days=365)
118 112
119 113 ts_min_m = mktime(td_1m.timetuple())
120 114 ts_min_y = mktime(td_1y.timetuple())
121 115 ts_max_y = mktime(td.timetuple())
122 116
123 117 if dbrepo.enable_statistics:
124 118 c.no_data_msg = _('No data loaded yet')
125 119 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
126 120 else:
127 121 c.no_data_msg = _('Statistics are disabled for this repository')
128 122 c.ts_min = ts_min_m
129 123 c.ts_max = ts_max_y
130 124
131 125 stats = self.sa.query(Statistics)\
132 126 .filter(Statistics.repository == dbrepo)\
133 127 .scalar()
134 128
135 129 c.stats_percentage = 0
136 130
137 131 if stats and stats.languages:
138 132 c.no_data = False is dbrepo.enable_statistics
139 133 lang_stats_d = json.loads(stats.languages)
140 134 c.commit_data = stats.commit_activity
141 135 c.overview_data = stats.commit_activity_combined
142
136
143 137 lang_stats = ((x, {"count": y,
144 138 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
145 139 for x, y in lang_stats_d.items())
146 140
147 141 c.trending_languages = json.dumps(OrderedDict(
148 142 sorted(lang_stats, reverse=True,
149 143 key=lambda k: k[1])[:10]
150 144 )
151 145 )
152 146 last_rev = stats.stat_on_revision
153 147 c.repo_last_rev = c.rhodecode_repo.count() - 1 \
154 148 if c.rhodecode_repo.revisions else 0
155 149 if last_rev == 0 or c.repo_last_rev == 0:
156 150 pass
157 151 else:
158 152 c.stats_percentage = '%.2f' % ((float((last_rev)) /
159 153 c.repo_last_rev) * 100)
160 154 else:
161 155 c.commit_data = json.dumps({})
162 156 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
163 157 c.trending_languages = json.dumps({})
164 158 c.no_data = True
165
159
166 160 c.enable_downloads = dbrepo.enable_downloads
167 161 if c.enable_downloads:
168 162 c.download_options = self._get_download_links(c.rhodecode_repo)
169 163
170 164 return render('summary/summary.html')
171 165
172 166 def _get_download_links(self, repo):
173 167
174 168 download_l = []
175 169
176 170 branches_group = ([], _("Branches"))
177 171 tags_group = ([], _("Tags"))
178 172
179 173 for name, chs in c.rhodecode_repo.branches.items():
180 174 #chs = chs.split(':')[-1]
181 175 branches_group[0].append((chs, name),)
182 176 download_l.append(branches_group)
183 177
184 178 for name, chs in c.rhodecode_repo.tags.items():
185 179 #chs = chs.split(':')[-1]
186 180 tags_group[0].append((chs, name),)
187 181 download_l.append(tags_group)
188 182
189 183 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 10 :copyright: (C) 2009-2011 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 from rhodecode.lib.odict import OrderedDict
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,389 +1,381 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) 2009-2010 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
27 try:
28 import json
29 except ImportError:
30 #python 2.5 compatibility
31 import simplejson as json
32
33
34 26 def __get_lem():
35 27 from pygments import lexers
36 28 from string import lower
37 29 from collections import defaultdict
38 30
39 31 d = defaultdict(lambda: [])
40 32
41 33 def __clean(s):
42 34 s = s.lstrip('*')
43 35 s = s.lstrip('.')
44 36
45 37 if s.find('[') != -1:
46 38 exts = []
47 39 start, stop = s.find('['), s.find(']')
48 40
49 41 for suffix in s[start + 1:stop]:
50 42 exts.append(s[:s.find('[')] + suffix)
51 43 return map(lower, exts)
52 44 else:
53 45 return map(lower, [s])
54 46
55 47 for lx, t in sorted(lexers.LEXERS.items()):
56 48 m = map(__clean, t[-2])
57 49 if m:
58 50 m = reduce(lambda x, y: x + y, m)
59 51 for ext in m:
60 52 desc = lx.replace('Lexer', '')
61 53 d[ext].append(desc)
62 54
63 55 return dict(d)
64 56
65 57 # language map is also used by whoosh indexer, which for those specified
66 58 # extensions will index it's content
67 59 LANGUAGES_EXTENSIONS_MAP = __get_lem()
68 60
69 61 # Additional mappings that are not present in the pygments lexers
70 62 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
71 63 ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}
72 64
73 65 LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
74 66
75 67
76 68 def str2bool(_str):
77 69 """
78 70 returs True/False value from given string, it tries to translate the
79 71 string into boolean
80 72
81 73 :param _str: string value to translate into boolean
82 74 :rtype: boolean
83 75 :returns: boolean from given string
84 76 """
85 77 if _str is None:
86 78 return False
87 79 if _str in (True, False):
88 80 return _str
89 81 _str = str(_str).strip().lower()
90 82 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
91 83
92 84
93 85 def convert_line_endings(line, mode):
94 86 """
95 87 Converts a given line "line end" accordingly to given mode
96 88
97 89 Available modes are::
98 90 0 - Unix
99 91 1 - Mac
100 92 2 - DOS
101 93
102 94 :param line: given line to convert
103 95 :param mode: mode to convert to
104 96 :rtype: str
105 97 :return: converted line according to mode
106 98 """
107 99 from string import replace
108 100
109 101 if mode == 0:
110 102 line = replace(line, '\r\n', '\n')
111 103 line = replace(line, '\r', '\n')
112 104 elif mode == 1:
113 105 line = replace(line, '\r\n', '\r')
114 106 line = replace(line, '\n', '\r')
115 107 elif mode == 2:
116 108 import re
117 109 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
118 110 return line
119 111
120 112
121 113 def detect_mode(line, default):
122 114 """
123 115 Detects line break for given line, if line break couldn't be found
124 116 given default value is returned
125 117
126 118 :param line: str line
127 119 :param default: default
128 120 :rtype: int
129 121 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
130 122 """
131 123 if line.endswith('\r\n'):
132 124 return 2
133 125 elif line.endswith('\n'):
134 126 return 0
135 127 elif line.endswith('\r'):
136 128 return 1
137 129 else:
138 130 return default
139 131
140 132
141 133 def generate_api_key(username, salt=None):
142 134 """
143 135 Generates unique API key for given username, if salt is not given
144 136 it'll be generated from some random string
145 137
146 138 :param username: username as string
147 139 :param salt: salt to hash generate KEY
148 140 :rtype: str
149 141 :returns: sha1 hash from username+salt
150 142 """
151 143 from tempfile import _RandomNameSequence
152 144 import hashlib
153 145
154 146 if salt is None:
155 147 salt = _RandomNameSequence().next()
156 148
157 149 return hashlib.sha1(username + salt).hexdigest()
158 150
159 151
160 152 def safe_unicode(str_, from_encoding='utf8'):
161 153 """
162 154 safe unicode function. Does few trick to turn str_ into unicode
163 155
164 156 In case of UnicodeDecode error we try to return it with encoding detected
165 157 by chardet library if it fails fallback to unicode with errors replaced
166 158
167 159 :param str_: string to decode
168 160 :rtype: unicode
169 161 :returns: unicode object
170 162 """
171 163 if isinstance(str_, unicode):
172 164 return str_
173 165
174 166 try:
175 167 return unicode(str_)
176 168 except UnicodeDecodeError:
177 169 pass
178 170
179 171 try:
180 172 return unicode(str_, from_encoding)
181 173 except UnicodeDecodeError:
182 174 pass
183 175
184 176 try:
185 177 import chardet
186 178 encoding = chardet.detect(str_)['encoding']
187 179 if encoding is None:
188 180 raise Exception()
189 181 return str_.decode(encoding)
190 182 except (ImportError, UnicodeDecodeError, Exception):
191 183 return unicode(str_, from_encoding, 'replace')
192 184
193 185 def safe_str(unicode_, to_encoding='utf8'):
194 186 """
195 187 safe str function. Does few trick to turn unicode_ into string
196 188
197 189 In case of UnicodeEncodeError we try to return it with encoding detected
198 190 by chardet library if it fails fallback to string with errors replaced
199 191
200 192 :param unicode_: unicode to encode
201 193 :rtype: str
202 194 :returns: str object
203 195 """
204 196
205 197 if isinstance(unicode_, str):
206 198 return unicode_
207 199
208 200 try:
209 201 return unicode_.encode(to_encoding)
210 202 except UnicodeEncodeError:
211 203 pass
212
204
213 205 try:
214 206 import chardet
215 207 encoding = chardet.detect(unicode_)['encoding']
216 208 print encoding
217 209 if encoding is None:
218 210 raise UnicodeEncodeError()
219
211
220 212 return unicode_.encode(encoding)
221 213 except (ImportError, UnicodeEncodeError):
222 214 return unicode_.encode(to_encoding, 'replace')
223 215
224 216 return safe_str
225 217
226 218
227 219
228 220 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
229 221 """
230 222 Custom engine_from_config functions that makes sure we use NullPool for
231 223 file based sqlite databases. This prevents errors on sqlite. This only
232 224 applies to sqlalchemy versions < 0.7.0
233 225
234 226 """
235 227 import sqlalchemy
236 228 from sqlalchemy import engine_from_config as efc
237 229 import logging
238 230
239 231 if int(sqlalchemy.__version__.split('.')[1]) < 7:
240 232
241 233 # This solution should work for sqlalchemy < 0.7.0, and should use
242 234 # proxy=TimerProxy() for execution time profiling
243 235
244 236 from sqlalchemy.pool import NullPool
245 237 url = configuration[prefix + 'url']
246 238
247 239 if url.startswith('sqlite'):
248 240 kwargs.update({'poolclass': NullPool})
249 241 return efc(configuration, prefix, **kwargs)
250 242 else:
251 243 import time
252 244 from sqlalchemy import event
253 245 from sqlalchemy.engine import Engine
254 246
255 247 log = logging.getLogger('sqlalchemy.engine')
256 248 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
257 249 engine = efc(configuration, prefix, **kwargs)
258 250
259 251 def color_sql(sql):
260 252 COLOR_SEQ = "\033[1;%dm"
261 253 COLOR_SQL = YELLOW
262 254 normal = '\x1b[0m'
263 255 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
264 256
265 257 if configuration['debug']:
266 258 #attach events only for debug configuration
267 259
268 260 def before_cursor_execute(conn, cursor, statement,
269 261 parameters, context, executemany):
270 262 context._query_start_time = time.time()
271 263 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
272 264
273 265
274 266 def after_cursor_execute(conn, cursor, statement,
275 267 parameters, context, executemany):
276 268 total = time.time() - context._query_start_time
277 269 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
278 270
279 271 event.listen(engine, "before_cursor_execute",
280 272 before_cursor_execute)
281 273 event.listen(engine, "after_cursor_execute",
282 274 after_cursor_execute)
283 275
284 276 return engine
285 277
286 278
287 279 def age(curdate):
288 280 """
289 281 turns a datetime into an age string.
290 282
291 283 :param curdate: datetime object
292 284 :rtype: unicode
293 285 :returns: unicode words describing age
294 286 """
295 287
296 288 from datetime import datetime
297 289 from webhelpers.date import time_ago_in_words
298 290
299 291 _ = lambda s:s
300 292
301 293 if not curdate:
302 294 return ''
303 295
304 296 agescales = [(_(u"year"), 3600 * 24 * 365),
305 297 (_(u"month"), 3600 * 24 * 30),
306 298 (_(u"day"), 3600 * 24),
307 299 (_(u"hour"), 3600),
308 300 (_(u"minute"), 60),
309 301 (_(u"second"), 1), ]
310 302
311 303 age = datetime.now() - curdate
312 304 age_seconds = (age.days * agescales[2][1]) + age.seconds
313 305 pos = 1
314 306 for scale in agescales:
315 307 if scale[1] <= age_seconds:
316 308 if pos == 6:pos = 5
317 309 return '%s %s' % (time_ago_in_words(curdate,
318 310 agescales[pos][0]), _('ago'))
319 311 pos += 1
320 312
321 313 return _(u'just now')
322 314
323 315
324 316 def uri_filter(uri):
325 317 """
326 318 Removes user:password from given url string
327 319
328 320 :param uri:
329 321 :rtype: unicode
330 322 :returns: filtered list of strings
331 323 """
332 324 if not uri:
333 325 return ''
334 326
335 327 proto = ''
336 328
337 329 for pat in ('https://', 'http://'):
338 330 if uri.startswith(pat):
339 331 uri = uri[len(pat):]
340 332 proto = pat
341 333 break
342 334
343 335 # remove passwords and username
344 336 uri = uri[uri.find('@') + 1:]
345 337
346 338 # get the port
347 339 cred_pos = uri.find(':')
348 340 if cred_pos == -1:
349 341 host, port = uri, None
350 342 else:
351 343 host, port = uri[:cred_pos], uri[cred_pos + 1:]
352 344
353 345 return filter(None, [proto, host, port])
354 346
355 347
356 348 def credentials_filter(uri):
357 349 """
358 350 Returns a url with removed credentials
359 351
360 352 :param uri:
361 353 """
362 354
363 355 uri = uri_filter(uri)
364 356 #check if we have port
365 357 if len(uri) > 2 and uri[2]:
366 358 uri[2] = ':' + uri[2]
367 359
368 360 return ''.join(uri)
369 361
370 362 def get_changeset_safe(repo, rev):
371 363 """
372 364 Safe version of get_changeset if this changeset doesn't exists for a
373 365 repo it returns a Dummy one instead
374 366
375 367 :param repo:
376 368 :param rev:
377 369 """
378 370 from vcs.backends.base import BaseRepository
379 371 from vcs.exceptions import RepositoryError
380 372 if not isinstance(repo, BaseRepository):
381 373 raise Exception('You must pass an Repository '
382 374 'object as first argument got %s', type(repo))
383 375
384 376 try:
385 377 cs = repo.get_changeset(rev)
386 378 except RepositoryError:
387 379 from rhodecode.lib.utils import EmptyChangeset
388 380 cs = EmptyChangeset(requested_revision=rev)
389 return cs No newline at end of file
381 return cs
@@ -1,413 +1,410 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) 2009-2011 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 31 from os.path import dirname as dn, 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 40 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
41 41 from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
42 42 __get_lockkey, LockHeld, DaemonLock
43 43 from rhodecode.lib.helpers import person
44 44 from rhodecode.lib.smtp_mailer import SmtpMailer
45 45 from rhodecode.lib.utils import add_cache
46 from rhodecode.lib.odict import OrderedDict
46 from rhodecode.lib.compat import json, OrderedDict
47
47 48 from rhodecode.model import init_model
48 49 from rhodecode.model import meta
49 50 from rhodecode.model.db import RhodeCodeUi, Statistics, Repository
50 51
51 52 from vcs.backends import get_repo
52 53
53 54 from sqlalchemy import engine_from_config
54 55
55 56 add_cache(config)
56 57
57 try:
58 import json
59 except ImportError:
60 #python 2.5 compatibility
61 import simplejson as json
58
62 59
63 60 __all__ = ['whoosh_index', 'get_commits_stats',
64 61 'reset_user_password', 'send_email']
65 62
66 63 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
67 64
68 65
69 66 def get_session():
70 67 if CELERY_ON:
71 68 engine = engine_from_config(config, 'sqlalchemy.db1.')
72 69 init_model(engine)
73 70 sa = meta.Session()
74 71 return sa
75 72
76 73
77 74 def get_repos_path():
78 75 sa = get_session()
79 76 q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
80 77 return q.ui_value
81 78
82 79
83 80 @task(ignore_result=True)
84 81 @locked_task
85 82 def whoosh_index(repo_location, full_index):
86 83 #log = whoosh_index.get_logger()
87 84 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
88 85 index_location = config['index_dir']
89 86 WhooshIndexingDaemon(index_location=index_location,
90 87 repo_location=repo_location, sa=get_session())\
91 88 .run(full_index=full_index)
92 89
93 90
94 91 @task(ignore_result=True)
95 92 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
96 93 try:
97 94 log = get_commits_stats.get_logger()
98 95 except:
99 96 log = logging.getLogger(__name__)
100 97
101 98 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
102 99 ts_max_y)
103 100 lockkey_path = dn(dn(dn(dn(os.path.abspath(__file__)))))
104 101 log.info('running task with lockkey %s', lockkey)
105 102 try:
106 103 lock = l = DaemonLock(jn(lockkey_path, lockkey))
107 104
108 105 #for js data compatibilty cleans the key for person from '
109 106 akc = lambda k: person(k).replace('"', "")
110 107
111 108 co_day_auth_aggr = {}
112 109 commits_by_day_aggregate = {}
113 110 repos_path = get_repos_path()
114 111 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
115 112 repo_size = len(repo.revisions)
116 113 #return if repo have no revisions
117 114 if repo_size < 1:
118 115 lock.release()
119 116 return True
120 117
121 118 skip_date_limit = True
122 119 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
123 120 last_rev = 0
124 121 last_cs = None
125 122 timegetter = itemgetter('time')
126 123
127 124 sa = get_session()
128 125
129 126 dbrepo = sa.query(Repository)\
130 127 .filter(Repository.repo_name == repo_name).scalar()
131 128 cur_stats = sa.query(Statistics)\
132 129 .filter(Statistics.repository == dbrepo).scalar()
133 130
134 131 if cur_stats is not None:
135 132 last_rev = cur_stats.stat_on_revision
136 133
137 134 if last_rev == repo.get_changeset().revision and repo_size > 1:
138 135 #pass silently without any work if we're not on first revision or
139 136 #current state of parsing revision(from db marker) is the
140 137 #last revision
141 138 lock.release()
142 139 return True
143 140
144 141 if cur_stats:
145 142 commits_by_day_aggregate = OrderedDict(json.loads(
146 143 cur_stats.commit_activity_combined))
147 144 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
148 145
149 146 log.debug('starting parsing %s', parse_limit)
150 147 lmktime = mktime
151 148
152 149 last_rev = last_rev + 1 if last_rev > 0 else last_rev
153 150
154 151 for cs in repo[last_rev:last_rev + parse_limit]:
155 152 last_cs = cs # remember last parsed changeset
156 153 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
157 154 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
158 155
159 156 if akc(cs.author) in co_day_auth_aggr:
160 157 try:
161 158 l = [timegetter(x) for x in
162 159 co_day_auth_aggr[akc(cs.author)]['data']]
163 160 time_pos = l.index(k)
164 161 except ValueError:
165 162 time_pos = False
166 163
167 164 if time_pos >= 0 and time_pos is not False:
168 165
169 166 datadict = \
170 167 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
171 168
172 169 datadict["commits"] += 1
173 170 datadict["added"] += len(cs.added)
174 171 datadict["changed"] += len(cs.changed)
175 172 datadict["removed"] += len(cs.removed)
176 173
177 174 else:
178 175 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
179 176
180 177 datadict = {"time": k,
181 178 "commits": 1,
182 179 "added": len(cs.added),
183 180 "changed": len(cs.changed),
184 181 "removed": len(cs.removed),
185 182 }
186 183 co_day_auth_aggr[akc(cs.author)]['data']\
187 184 .append(datadict)
188 185
189 186 else:
190 187 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
191 188 co_day_auth_aggr[akc(cs.author)] = {
192 189 "label": akc(cs.author),
193 190 "data": [{"time":k,
194 191 "commits":1,
195 192 "added":len(cs.added),
196 193 "changed":len(cs.changed),
197 194 "removed":len(cs.removed),
198 195 }],
199 196 "schema": ["commits"],
200 197 }
201 198
202 199 #gather all data by day
203 200 if k in commits_by_day_aggregate:
204 201 commits_by_day_aggregate[k] += 1
205 202 else:
206 203 commits_by_day_aggregate[k] = 1
207 204
208 205 overview_data = sorted(commits_by_day_aggregate.items(),
209 206 key=itemgetter(0))
210 207
211 208 if not co_day_auth_aggr:
212 209 co_day_auth_aggr[akc(repo.contact)] = {
213 210 "label": akc(repo.contact),
214 211 "data": [0, 1],
215 212 "schema": ["commits"],
216 213 }
217 214
218 215 stats = cur_stats if cur_stats else Statistics()
219 216 stats.commit_activity = json.dumps(co_day_auth_aggr)
220 217 stats.commit_activity_combined = json.dumps(overview_data)
221 218
222 219 log.debug('last revison %s', last_rev)
223 220 leftovers = len(repo.revisions[last_rev:])
224 221 log.debug('revisions to parse %s', leftovers)
225 222
226 223 if last_rev == 0 or leftovers < parse_limit:
227 224 log.debug('getting code trending stats')
228 225 stats.languages = json.dumps(__get_codes_stats(repo_name))
229 226
230 227 try:
231 228 stats.repository = dbrepo
232 229 stats.stat_on_revision = last_cs.revision if last_cs else 0
233 230 sa.add(stats)
234 231 sa.commit()
235 232 except:
236 233 log.error(traceback.format_exc())
237 234 sa.rollback()
238 235 lock.release()
239 236 return False
240 237
241 238 #final release
242 239 lock.release()
243 240
244 241 #execute another task if celery is enabled
245 242 if len(repo.revisions) > 1 and CELERY_ON:
246 243 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y)
247 244 return True
248 245 except LockHeld:
249 246 log.info('LockHeld')
250 247 return 'Task with key %s already running' % lockkey
251 248
252 249 @task(ignore_result=True)
253 250 def send_password_link(user_email):
254 251 try:
255 252 log = reset_user_password.get_logger()
256 253 except:
257 254 log = logging.getLogger(__name__)
258 255
259 256 from rhodecode.lib import auth
260 257 from rhodecode.model.db import User
261 258
262 259 try:
263 260 sa = get_session()
264 261 user = sa.query(User).filter(User.email == user_email).scalar()
265 262
266 263 if user:
267 264 link = url('reset_password_confirmation', key=user.api_key,
268 265 qualified=True)
269 266 tmpl = """
270 267 Hello %s
271 268
272 269 We received a request to create a new password for your account.
273 270
274 271 You can generate it by clicking following URL:
275 272
276 273 %s
277 274
278 275 If you didn't request new password please ignore this email.
279 276 """
280 277 run_task(send_email, user_email,
281 278 "RhodeCode password reset link",
282 279 tmpl % (user.short_contact, link))
283 280 log.info('send new password mail to %s', user_email)
284 281
285 282 except:
286 283 log.error('Failed to update user password')
287 284 log.error(traceback.format_exc())
288 285 return False
289 286
290 287 return True
291 288
292 289 @task(ignore_result=True)
293 290 def reset_user_password(user_email):
294 291 try:
295 292 log = reset_user_password.get_logger()
296 293 except:
297 294 log = logging.getLogger(__name__)
298 295
299 296 from rhodecode.lib import auth
300 297 from rhodecode.model.db import User
301 298
302 299 try:
303 300 try:
304 301 sa = get_session()
305 302 user = sa.query(User).filter(User.email == user_email).scalar()
306 303 new_passwd = auth.PasswordGenerator().gen_password(8,
307 304 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
308 305 if user:
309 306 user.password = auth.get_crypt_password(new_passwd)
310 307 user.api_key = auth.generate_api_key(user.username)
311 308 sa.add(user)
312 309 sa.commit()
313 310 log.info('change password for %s', user_email)
314 311 if new_passwd is None:
315 312 raise Exception('unable to generate new password')
316 313
317 314 except:
318 315 log.error(traceback.format_exc())
319 316 sa.rollback()
320 317
321 318 run_task(send_email, user_email,
322 319 "Your new RhodeCode password",
323 320 'Your new RhodeCode password:%s' % (new_passwd))
324 321 log.info('send new password mail to %s', user_email)
325 322
326 323 except:
327 324 log.error('Failed to update user password')
328 325 log.error(traceback.format_exc())
329 326
330 327 return True
331 328
332 329
333 330 @task(ignore_result=True)
334 331 def send_email(recipients, subject, body):
335 332 """
336 333 Sends an email with defined parameters from the .ini files.
337 334
338 335 :param recipients: list of recipients, it this is empty the defined email
339 336 address from field 'email_to' is used instead
340 337 :param subject: subject of the mail
341 338 :param body: body of the mail
342 339 """
343 340 try:
344 341 log = send_email.get_logger()
345 342 except:
346 343 log = logging.getLogger(__name__)
347 344
348 345 email_config = config
349 346
350 347 if not recipients:
351 348 recipients = [email_config.get('email_to')]
352 349
353 350 mail_from = email_config.get('app_email_from')
354 351 user = email_config.get('smtp_username')
355 352 passwd = email_config.get('smtp_password')
356 353 mail_server = email_config.get('smtp_server')
357 354 mail_port = email_config.get('smtp_port')
358 355 tls = str2bool(email_config.get('smtp_use_tls'))
359 356 ssl = str2bool(email_config.get('smtp_use_ssl'))
360 357 debug = str2bool(config.get('debug'))
361 358
362 359 try:
363 360 m = SmtpMailer(mail_from, user, passwd, mail_server,
364 361 mail_port, ssl, tls, debug=debug)
365 362 m.send(recipients, subject, body)
366 363 except:
367 364 log.error('Mail sending failed')
368 365 log.error(traceback.format_exc())
369 366 return False
370 367 return True
371 368
372 369
373 370 @task(ignore_result=True)
374 371 def create_repo_fork(form_data, cur_user):
375 372 from rhodecode.model.repo import RepoModel
376 373 from vcs import get_backend
377 374
378 375 try:
379 376 log = create_repo_fork.get_logger()
380 377 except:
381 378 log = logging.getLogger(__name__)
382 379
383 380 repo_model = RepoModel(get_session())
384 381 repo_model.create(form_data, cur_user, just_db=True, fork=True)
385 382 repo_name = form_data['repo_name']
386 383 repos_path = get_repos_path()
387 384 repo_path = os.path.join(repos_path, repo_name)
388 385 repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
389 386 alias = form_data['repo_type']
390 387
391 388 log.info('creating repo fork %s as %s', repo_name, repo_path)
392 389 backend = get_backend(alias)
393 390 backend(str(repo_fork_path), create=True, src_url=str(repo_path))
394 391
395 392
396 393 def __get_codes_stats(repo_name):
397 394 repos_path = get_repos_path()
398 395 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
399 396 tip = repo.get_changeset()
400 397 code_stats = {}
401 398
402 399 def aggregate(cs):
403 400 for f in cs[2]:
404 401 ext = lower(f.extension)
405 402 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
406 403 if ext in code_stats:
407 404 code_stats[ext] += 1
408 405 else:
409 406 code_stats[ext] = 1
410 407
411 408 map(aggregate, tip.walk('/'))
412 409
413 410 return code_stats or {}
@@ -1,979 +1,980 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 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 datetime
29 29 import traceback
30 30 from datetime import date
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.exc import DatabaseError
34 34 from sqlalchemy.orm import relationship, backref, joinedload, class_mapper
35 35 from sqlalchemy.orm.interfaces import MapperExtension
36 36
37 37 from beaker.cache import cache_region, region_invalidate
38 38
39 39 from vcs import get_backend
40 40 from vcs.utils.helpers import get_scm
41 41 from vcs.exceptions import VCSError
42 42 from vcs.utils.lazy import LazyProperty
43 43
44 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \
45 generate_api_key
44 46 from rhodecode.lib.exceptions import UsersGroupsAssignedException
45 from rhodecode.lib import str2bool, json, safe_str, get_changeset_safe,\
46 generate_api_key
47 from rhodecode.lib.compat import json
47 48
48 49 from rhodecode.model.meta import Base, Session
49 50 from rhodecode.model.caching_query import FromCache
50 51
51 52 log = logging.getLogger(__name__)
52 53
53 54 #==============================================================================
54 55 # BASE CLASSES
55 56 #==============================================================================
56 57
57 58 class ModelSerializer(json.JSONEncoder):
58 59 """
59 60 Simple Serializer for JSON,
60 61
61 62 usage::
62 63
63 64 to make object customized for serialization implement a __json__
64 65 method that will return a dict for serialization into json
65 66
66 67 example::
67 68
68 69 class Task(object):
69 70
70 71 def __init__(self, name, value):
71 72 self.name = name
72 73 self.value = value
73 74
74 75 def __json__(self):
75 76 return dict(name=self.name,
76 77 value=self.value)
77 78
78 79 """
79 80
80 81 def default(self, obj):
81 82
82 83 if hasattr(obj, '__json__'):
83 84 return obj.__json__()
84 85 else:
85 86 return json.JSONEncoder.default(self, obj)
86 87
87 88 class BaseModel(object):
88 89 """Base Model for all classess
89 90
90 91 """
91 92
92 93 @classmethod
93 94 def _get_keys(cls):
94 95 """return column names for this model """
95 96 return class_mapper(cls).c.keys()
96 97
97 98 def get_dict(self):
98 99 """return dict with keys and values corresponding
99 100 to this model data """
100 101
101 102 d = {}
102 103 for k in self._get_keys():
103 104 d[k] = getattr(self, k)
104 105 return d
105 106
106 107 def get_appstruct(self):
107 108 """return list with keys and values tupples corresponding
108 109 to this model data """
109 110
110 111 l = []
111 112 for k in self._get_keys():
112 113 l.append((k, getattr(self, k),))
113 114 return l
114 115
115 116 def populate_obj(self, populate_dict):
116 117 """populate model with data from given populate_dict"""
117 118
118 119 for k in self._get_keys():
119 120 if k in populate_dict:
120 121 setattr(self, k, populate_dict[k])
121 122
122 123 @classmethod
123 124 def query(cls):
124 125 return Session.query(cls)
125 126
126 127 @classmethod
127 128 def get(cls, id_):
128 129 return Session.query(cls).get(id_)
129 130
130 131 @classmethod
131 132 def delete(cls, id_):
132 133 obj = Session.query(cls).get(id_)
133 134 Session.delete(obj)
134 135 Session.commit()
135 136
136 137
137 138 class RhodeCodeSettings(Base, BaseModel):
138 139 __tablename__ = 'rhodecode_settings'
139 140 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
140 141 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
141 142 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
142 143 app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
143 144
144 145 def __init__(self, k='', v=''):
145 146 self.app_settings_name = k
146 147 self.app_settings_value = v
147 148
148 149 def __repr__(self):
149 150 return "<%s('%s:%s')>" % (self.__class__.__name__,
150 151 self.app_settings_name, self.app_settings_value)
151 152
152 153
153 154 @classmethod
154 155 def get_by_name(cls, ldap_key):
155 156 return Session.query(cls)\
156 157 .filter(cls.app_settings_name == ldap_key).scalar()
157 158
158 159 @classmethod
159 160 def get_app_settings(cls, cache=False):
160 161
161 162 ret = Session.query(cls)
162 163
163 164 if cache:
164 165 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
165 166
166 167 if not ret:
167 168 raise Exception('Could not get application settings !')
168 169 settings = {}
169 170 for each in ret:
170 171 settings['rhodecode_' + each.app_settings_name] = \
171 172 each.app_settings_value
172 173
173 174 return settings
174 175
175 176 @classmethod
176 177 def get_ldap_settings(cls, cache=False):
177 178 ret = Session.query(cls)\
178 179 .filter(cls.app_settings_name.startswith('ldap_'))\
179 180 .all()
180 181 fd = {}
181 182 for row in ret:
182 183 fd.update({row.app_settings_name:row.app_settings_value})
183 184
184 185 fd.update({'ldap_active':str2bool(fd.get('ldap_active'))})
185 186
186 187 return fd
187 188
188 189
189 190 class RhodeCodeUi(Base, BaseModel):
190 191 __tablename__ = 'rhodecode_ui'
191 192 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
192 193
193 194 HOOK_UPDATE = 'changegroup.update'
194 195 HOOK_REPO_SIZE = 'changegroup.repo_size'
195 196 HOOK_PUSH = 'pretxnchangegroup.push_logger'
196 197 HOOK_PULL = 'preoutgoing.pull_logger'
197 198
198 199 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
199 200 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
200 201 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
201 202 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
202 203 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
203 204
204 205
205 206 @classmethod
206 207 def get_by_key(cls, key):
207 208 return Session.query(cls).filter(cls.ui_key == key)
208 209
209 210
210 211 @classmethod
211 212 def get_builtin_hooks(cls):
212 213 q = cls.query()
213 214 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
214 215 cls.HOOK_REPO_SIZE,
215 216 cls.HOOK_PUSH, cls.HOOK_PULL]))
216 217 return q.all()
217 218
218 219 @classmethod
219 220 def get_custom_hooks(cls):
220 221 q = cls.query()
221 222 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
222 223 cls.HOOK_REPO_SIZE,
223 224 cls.HOOK_PUSH, cls.HOOK_PULL]))
224 225 q = q.filter(cls.ui_section == 'hooks')
225 226 return q.all()
226 227
227 228 @classmethod
228 229 def create_or_update_hook(cls, key, val):
229 230 new_ui = cls.get_by_key(key).scalar() or cls()
230 231 new_ui.ui_section = 'hooks'
231 232 new_ui.ui_active = True
232 233 new_ui.ui_key = key
233 234 new_ui.ui_value = val
234 235
235 236 Session.add(new_ui)
236 237 Session.commit()
237 238
238 239
239 240 class User(Base, BaseModel):
240 241 __tablename__ = 'users'
241 242 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
242 243 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
243 244 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
244 245 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
245 246 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
246 247 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
247 248 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
248 249 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
249 250 email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
250 251 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
251 252 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
252 253 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
253 254
254 255 user_log = relationship('UserLog', cascade='all')
255 256 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
256 257
257 258 repositories = relationship('Repository')
258 259 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
259 260 repo_to_perm = relationship('RepoToPerm', primaryjoin='RepoToPerm.user_id==User.user_id', cascade='all')
260 261
261 262 group_member = relationship('UsersGroupMember', cascade='all')
262 263
263 264 @property
264 265 def full_contact(self):
265 266 return '%s %s <%s>' % (self.name, self.lastname, self.email)
266 267
267 268 @property
268 269 def short_contact(self):
269 270 return '%s %s' % (self.name, self.lastname)
270 271
271 272 @property
272 273 def is_admin(self):
273 274 return self.admin
274 275
275 276 def __repr__(self):
276 277 try:
277 278 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
278 279 self.user_id, self.username)
279 280 except:
280 281 return self.__class__.__name__
281 282
282 283 @classmethod
283 284 def by_username(cls, username, case_insensitive=False):
284 285 if case_insensitive:
285 286 return Session.query(cls).filter(cls.username.like(username)).one()
286 287 else:
287 288 return Session.query(cls).filter(cls.username == username).one()
288 289
289 290 @classmethod
290 291 def get_by_api_key(cls, api_key):
291 292 return Session.query(cls).filter(cls.api_key == api_key).one()
292 293
293 294
294 295 def update_lastlogin(self):
295 296 """Update user lastlogin"""
296 297
297 298 self.last_login = datetime.datetime.now()
298 299 Session.add(self)
299 300 Session.commit()
300 301 log.debug('updated user %s lastlogin', self.username)
301 302
302 303 @classmethod
303 304 def create(cls, form_data):
304 305 from rhodecode.lib.auth import get_crypt_password
305
306
306 307 try:
307 308 new_user = cls()
308 309 for k, v in form_data.items():
309 310 if k == 'password':
310 311 v = get_crypt_password(v)
311 312 setattr(new_user, k, v)
312 313
313 314 new_user.api_key = generate_api_key(form_data['username'])
314 315 Session.add(new_user)
315 316 Session.commit()
316 317 return new_user
317 318 except:
318 319 log.error(traceback.format_exc())
319 320 Session.rollback()
320 321 raise
321 322
322 323 class UserLog(Base, BaseModel):
323 324 __tablename__ = 'user_logs'
324 325 __table_args__ = {'extend_existing':True}
325 326 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
326 327 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
327 328 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
328 329 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 330 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 331 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 332 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
332 333
333 334 @property
334 335 def action_as_day(self):
335 336 return date(*self.action_date.timetuple()[:3])
336 337
337 338 user = relationship('User')
338 339 repository = relationship('Repository')
339 340
340 341
341 342 class UsersGroup(Base, BaseModel):
342 343 __tablename__ = 'users_groups'
343 344 __table_args__ = {'extend_existing':True}
344 345
345 346 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
346 347 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
347 348 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
348 349
349 350 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
350 351
351 352 def __repr__(self):
352 353 return '<userGroup(%s)>' % (self.users_group_name)
353 354
354 355 @classmethod
355 356 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
356 357 if case_insensitive:
357 358 gr = Session.query(cls)\
358 359 .filter(cls.users_group_name.ilike(group_name))
359 360 else:
360 361 gr = Session.query(UsersGroup)\
361 362 .filter(UsersGroup.users_group_name == group_name)
362 363 if cache:
363 364 gr = gr.options(FromCache("sql_cache_short",
364 365 "get_user_%s" % group_name))
365 366 return gr.scalar()
366 367
367 368
368 369 @classmethod
369 370 def get(cls, users_group_id, cache=False):
370 371 users_group = Session.query(cls)
371 372 if cache:
372 373 users_group = users_group.options(FromCache("sql_cache_short",
373 374 "get_users_group_%s" % users_group_id))
374 375 return users_group.get(users_group_id)
375 376
376 377 @classmethod
377 378 def create(cls, form_data):
378 379 try:
379 380 new_users_group = cls()
380 381 for k, v in form_data.items():
381 382 setattr(new_users_group, k, v)
382 383
383 384 Session.add(new_users_group)
384 385 Session.commit()
385 386 return new_users_group
386 387 except:
387 388 log.error(traceback.format_exc())
388 389 Session.rollback()
389 390 raise
390 391
391 392 @classmethod
392 393 def update(cls, users_group_id, form_data):
393 394
394 395 try:
395 396 users_group = cls.get(users_group_id, cache=False)
396 397
397 398 for k, v in form_data.items():
398 399 if k == 'users_group_members':
399 400 users_group.members = []
400 401 Session.flush()
401 402 members_list = []
402 403 if v:
403 404 for u_id in set(v):
404 405 members_list.append(UsersGroupMember(
405 406 users_group_id,
406 407 u_id))
407 408 setattr(users_group, 'members', members_list)
408 409 setattr(users_group, k, v)
409 410
410 411 Session.add(users_group)
411 412 Session.commit()
412 413 except:
413 414 log.error(traceback.format_exc())
414 415 Session.rollback()
415 416 raise
416 417
417 418 @classmethod
418 419 def delete(cls, users_group_id):
419 420 try:
420 421
421 422 # check if this group is not assigned to repo
422 423 assigned_groups = UsersGroupRepoToPerm.query()\
423 424 .filter(UsersGroupRepoToPerm.users_group_id ==
424 425 users_group_id).all()
425 426
426 427 if assigned_groups:
427 428 raise UsersGroupsAssignedException('Group assigned to %s' %
428 429 assigned_groups)
429 430
430 431 users_group = cls.get(users_group_id, cache=False)
431 432 Session.delete(users_group)
432 433 Session.commit()
433 434 except:
434 435 log.error(traceback.format_exc())
435 436 Session.rollback()
436 437 raise
437 438
438 439
439 440 class UsersGroupMember(Base, BaseModel):
440 441 __tablename__ = 'users_groups_members'
441 442 __table_args__ = {'extend_existing':True}
442 443
443 444 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
444 445 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
445 446 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
446 447
447 448 user = relationship('User', lazy='joined')
448 449 users_group = relationship('UsersGroup')
449 450
450 451 def __init__(self, gr_id='', u_id=''):
451 452 self.users_group_id = gr_id
452 453 self.user_id = u_id
453 454
454 455 class Repository(Base, BaseModel):
455 456 __tablename__ = 'repositories'
456 457 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
457 458
458 459 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
459 460 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
460 461 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
461 462 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
462 463 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
463 464 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
464 465 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
465 466 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
466 467 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
467 468 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
468 469
469 470 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
470 471 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
471 472
472 473
473 474 user = relationship('User')
474 475 fork = relationship('Repository', remote_side=repo_id)
475 476 group = relationship('Group')
476 477 repo_to_perm = relationship('RepoToPerm', cascade='all', order_by='RepoToPerm.repo_to_perm_id')
477 478 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
478 479 stats = relationship('Statistics', cascade='all', uselist=False)
479 480
480 481 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
481 482
482 483 logs = relationship('UserLog', cascade='all')
483 484
484 485 def __repr__(self):
485 486 return "<%s('%s:%s')>" % (self.__class__.__name__,
486 487 self.repo_id, self.repo_name)
487 488
488 489 @classmethod
489 490 def by_repo_name(cls, repo_name):
490 491 q = Session.query(cls).filter(cls.repo_name == repo_name)
491 492
492 493 q = q.options(joinedload(Repository.fork))\
493 494 .options(joinedload(Repository.user))\
494 495 .options(joinedload(Repository.group))\
495 496
496 497 return q.one()
497 498
498 499 @classmethod
499 500 def get_repo_forks(cls, repo_id):
500 501 return Session.query(cls).filter(Repository.fork_id == repo_id)
501 502
502 503 @classmethod
503 504 def base_path(cls):
504 505 """
505 506 Returns base path when all repos are stored
506 507
507 508 :param cls:
508 509 """
509 510 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/')
510 511 q.options(FromCache("sql_cache_short", "repository_repo_path"))
511 512 return q.one().ui_value
512 513
513 514 @property
514 515 def just_name(self):
515 516 return self.repo_name.split(os.sep)[-1]
516 517
517 518 @property
518 519 def groups_with_parents(self):
519 520 groups = []
520 521 if self.group is None:
521 522 return groups
522 523
523 524 cur_gr = self.group
524 525 groups.insert(0, cur_gr)
525 526 while 1:
526 527 gr = getattr(cur_gr, 'parent_group', None)
527 528 cur_gr = cur_gr.parent_group
528 529 if gr is None:
529 530 break
530 531 groups.insert(0, gr)
531 532
532 533 return groups
533 534
534 535 @property
535 536 def groups_and_repo(self):
536 537 return self.groups_with_parents, self.just_name
537 538
538 539 @LazyProperty
539 540 def repo_path(self):
540 541 """
541 542 Returns base full path for that repository means where it actually
542 543 exists on a filesystem
543 544 """
544 545 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/')
545 546 q.options(FromCache("sql_cache_short", "repository_repo_path"))
546 547 return q.one().ui_value
547 548
548 549 @property
549 550 def repo_full_path(self):
550 551 p = [self.repo_path]
551 552 # we need to split the name by / since this is how we store the
552 553 # names in the database, but that eventually needs to be converted
553 554 # into a valid system path
554 555 p += self.repo_name.split('/')
555 556 return os.path.join(*p)
556 557
557 558 @property
558 559 def _ui(self):
559 560 """
560 561 Creates an db based ui object for this repository
561 562 """
562 563 from mercurial import ui
563 564 from mercurial import config
564 565 baseui = ui.ui()
565 566
566 567 #clean the baseui object
567 568 baseui._ocfg = config.config()
568 569 baseui._ucfg = config.config()
569 570 baseui._tcfg = config.config()
570 571
571 572
572 573 ret = Session.query(RhodeCodeUi)\
573 574 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
574 575
575 576 hg_ui = ret
576 577 for ui_ in hg_ui:
577 578 if ui_.ui_active:
578 579 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
579 580 ui_.ui_key, ui_.ui_value)
580 581 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
581 582
582 583 return baseui
583 584
584 585 @classmethod
585 586 def is_valid(cls, repo_name):
586 587 """
587 588 returns True if given repo name is a valid filesystem repository
588 589
589 590 @param cls:
590 591 @param repo_name:
591 592 """
592 593 from rhodecode.lib.utils import is_valid_repo
593 594
594 595 return is_valid_repo(repo_name, cls.base_path())
595 596
596 597
597 598 #==========================================================================
598 599 # SCM PROPERTIES
599 600 #==========================================================================
600 601
601 602 def get_changeset(self, rev):
602 603 return get_changeset_safe(self.scm_instance, rev)
603 604
604 605 @property
605 606 def tip(self):
606 607 return self.get_changeset('tip')
607 608
608 609 @property
609 610 def author(self):
610 611 return self.tip.author
611 612
612 613 @property
613 614 def last_change(self):
614 615 return self.scm_instance.last_change
615 616
616 617 #==========================================================================
617 618 # SCM CACHE INSTANCE
618 619 #==========================================================================
619 620
620 621 @property
621 622 def invalidate(self):
622 623 """
623 624 Returns Invalidation object if this repo should be invalidated
624 625 None otherwise. `cache_active = False` means that this cache
625 626 state is not valid and needs to be invalidated
626 627 """
627 628 return Session.query(CacheInvalidation)\
628 629 .filter(CacheInvalidation.cache_key == self.repo_name)\
629 630 .filter(CacheInvalidation.cache_active == False)\
630 631 .scalar()
631 632
632 633 def set_invalidate(self):
633 634 """
634 635 set a cache for invalidation for this instance
635 636 """
636 637 inv = Session.query(CacheInvalidation)\
637 638 .filter(CacheInvalidation.cache_key == self.repo_name)\
638 639 .scalar()
639 640
640 641 if inv is None:
641 642 inv = CacheInvalidation(self.repo_name)
642 643 inv.cache_active = True
643 644 Session.add(inv)
644 645 Session.commit()
645 646
646 647 @LazyProperty
647 648 def scm_instance(self):
648 649 return self.__get_instance()
649 650
650 651 @property
651 652 def scm_instance_cached(self):
652 653 @cache_region('long_term')
653 654 def _c(repo_name):
654 655 return self.__get_instance()
655 656
656 657 # TODO: remove this trick when beaker 1.6 is released
657 658 # and have fixed this issue with not supporting unicode keys
658 659 rn = safe_str(self.repo_name)
659 660
660 661 inv = self.invalidate
661 662 if inv is not None:
662 663 region_invalidate(_c, None, rn)
663 664 # update our cache
664 665 inv.cache_active = True
665 666 Session.add(inv)
666 667 Session.commit()
667 668
668 669 return _c(rn)
669 670
670 671 def __get_instance(self):
671 672
672 673 repo_full_path = self.repo_full_path
673 674
674 675 try:
675 676 alias = get_scm(repo_full_path)[0]
676 677 log.debug('Creating instance of %s repository', alias)
677 678 backend = get_backend(alias)
678 679 except VCSError:
679 680 log.error(traceback.format_exc())
680 681 log.error('Perhaps this repository is in db and not in '
681 682 'filesystem run rescan repositories with '
682 683 '"destroy old data " option from admin panel')
683 684 return
684 685
685 686 if alias == 'hg':
686 687
687 688 repo = backend(safe_str(repo_full_path), create=False,
688 689 baseui=self._ui)
689 690 #skip hidden web repository
690 691 if repo._get_hidden():
691 692 return
692 693 else:
693 694 repo = backend(repo_full_path, create=False)
694 695
695 696 return repo
696 697
697 698
698 699 class Group(Base, BaseModel):
699 700 __tablename__ = 'groups'
700 701 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
701 702 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
702 703 __mapper_args__ = {'order_by':'group_name'}
703 704
704 705 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
705 706 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
706 707 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
707 708 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
708 709
709 710 parent_group = relationship('Group', remote_side=group_id)
710 711
711 712
712 713 def __init__(self, group_name='', parent_group=None):
713 714 self.group_name = group_name
714 715 self.parent_group = parent_group
715 716
716 717 def __repr__(self):
717 718 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
718 719 self.group_name)
719 720
720 721 @classmethod
721 722 def url_sep(cls):
722 723 return '/'
723 724
724 725 @property
725 726 def parents(self):
726 727 parents_recursion_limit = 5
727 728 groups = []
728 729 if self.parent_group is None:
729 730 return groups
730 731 cur_gr = self.parent_group
731 732 groups.insert(0, cur_gr)
732 733 cnt = 0
733 734 while 1:
734 735 cnt += 1
735 736 gr = getattr(cur_gr, 'parent_group', None)
736 737 cur_gr = cur_gr.parent_group
737 738 if gr is None:
738 739 break
739 740 if cnt == parents_recursion_limit:
740 741 # this will prevent accidental infinit loops
741 742 log.error('group nested more than %s' %
742 743 parents_recursion_limit)
743 744 break
744 745
745 746 groups.insert(0, gr)
746 747 return groups
747 748
748 749 @property
749 750 def children(self):
750 751 return Session.query(Group).filter(Group.parent_group == self)
751 752
752 753 @property
753 754 def full_path(self):
754 755 return Group.url_sep().join([g.group_name for g in self.parents] +
755 756 [self.group_name])
756 757
757 758 @property
758 759 def repositories(self):
759 760 return Session.query(Repository).filter(Repository.group == self)
760 761
761 762 @property
762 763 def repositories_recursive_count(self):
763 764 cnt = self.repositories.count()
764 765
765 766 def children_count(group):
766 767 cnt = 0
767 768 for child in group.children:
768 769 cnt += child.repositories.count()
769 770 cnt += children_count(child)
770 771 return cnt
771 772
772 773 return cnt + children_count(self)
773 774
774 775 class Permission(Base, BaseModel):
775 776 __tablename__ = 'permissions'
776 777 __table_args__ = {'extend_existing':True}
777 778 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
778 779 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
779 780 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
780 781
781 782 def __repr__(self):
782 783 return "<%s('%s:%s')>" % (self.__class__.__name__,
783 784 self.permission_id, self.permission_name)
784 785
785 786 @classmethod
786 787 def get_by_key(cls, key):
787 788 return Session.query(cls).filter(cls.permission_name == key).scalar()
788 789
789 790 class RepoToPerm(Base, BaseModel):
790 791 __tablename__ = 'repo_to_perm'
791 792 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
792 793 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
793 794 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
794 795 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
795 796 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
796 797
797 798 user = relationship('User')
798 799 permission = relationship('Permission')
799 800 repository = relationship('Repository')
800 801
801 802 class UserToPerm(Base, BaseModel):
802 803 __tablename__ = 'user_to_perm'
803 804 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
804 805 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
805 806 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
806 807 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
807 808
808 809 user = relationship('User')
809 810 permission = relationship('Permission')
810 811
811 812 @classmethod
812 813 def has_perm(cls, user_id, perm):
813 814 if not isinstance(perm, Permission):
814 815 raise Exception('perm needs to be an instance of Permission class')
815 816
816 817 return Session.query(cls).filter(cls.user_id == user_id)\
817 818 .filter(cls.permission == perm).scalar() is not None
818 819
819 820 @classmethod
820 821 def grant_perm(cls, user_id, perm):
821 822 if not isinstance(perm, Permission):
822 823 raise Exception('perm needs to be an instance of Permission class')
823 824
824 825 new = cls()
825 826 new.user_id = user_id
826 827 new.permission = perm
827 828 try:
828 829 Session.add(new)
829 830 Session.commit()
830 831 except:
831 832 Session.rollback()
832 833
833 834
834 835 @classmethod
835 836 def revoke_perm(cls, user_id, perm):
836 837 if not isinstance(perm, Permission):
837 838 raise Exception('perm needs to be an instance of Permission class')
838 839
839 840 try:
840 841 Session.query(cls).filter(cls.user_id == user_id)\
841 842 .filter(cls.permission == perm).delete()
842 843 Session.commit()
843 844 except:
844 845 Session.rollback()
845 846
846 847 class UsersGroupRepoToPerm(Base, BaseModel):
847 848 __tablename__ = 'users_group_repo_to_perm'
848 849 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
849 850 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
850 851 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
851 852 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
852 853 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
853 854
854 855 users_group = relationship('UsersGroup')
855 856 permission = relationship('Permission')
856 857 repository = relationship('Repository')
857 858
858 859 def __repr__(self):
859 860 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
860 861
861 862 class UsersGroupToPerm(Base, BaseModel):
862 863 __tablename__ = 'users_group_to_perm'
863 864 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
864 865 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
865 866 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
866 867
867 868 users_group = relationship('UsersGroup')
868 869 permission = relationship('Permission')
869 870
870 871
871 872 @classmethod
872 873 def has_perm(cls, users_group_id, perm):
873 874 if not isinstance(perm, Permission):
874 875 raise Exception('perm needs to be an instance of Permission class')
875 876
876 877 return Session.query(cls).filter(cls.users_group_id ==
877 878 users_group_id)\
878 879 .filter(cls.permission == perm)\
879 880 .scalar() is not None
880 881
881 882 @classmethod
882 883 def grant_perm(cls, users_group_id, perm):
883 884 if not isinstance(perm, Permission):
884 885 raise Exception('perm needs to be an instance of Permission class')
885 886
886 887 new = cls()
887 888 new.users_group_id = users_group_id
888 889 new.permission = perm
889 890 try:
890 891 Session.add(new)
891 892 Session.commit()
892 893 except:
893 894 Session.rollback()
894 895
895 896
896 897 @classmethod
897 898 def revoke_perm(cls, users_group_id, perm):
898 899 if not isinstance(perm, Permission):
899 900 raise Exception('perm needs to be an instance of Permission class')
900 901
901 902 try:
902 903 Session.query(cls).filter(cls.users_group_id == users_group_id)\
903 904 .filter(cls.permission == perm).delete()
904 905 Session.commit()
905 906 except:
906 907 Session.rollback()
907 908
908 909
909 910 class GroupToPerm(Base, BaseModel):
910 911 __tablename__ = 'group_to_perm'
911 912 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
912 913
913 914 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
914 915 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
915 916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
916 917 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
917 918
918 919 user = relationship('User')
919 920 permission = relationship('Permission')
920 921 group = relationship('Group')
921 922
922 923 class Statistics(Base, BaseModel):
923 924 __tablename__ = 'statistics'
924 925 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
925 926 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
926 927 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
927 928 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
928 929 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
929 930 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
930 931 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
931 932
932 933 repository = relationship('Repository', single_parent=True)
933 934
934 935 class UserFollowing(Base, BaseModel):
935 936 __tablename__ = 'user_followings'
936 937 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
937 938 UniqueConstraint('user_id', 'follows_user_id')
938 939 , {'extend_existing':True})
939 940
940 941 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
942 943 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
943 944 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
944 945 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
945 946
946 947 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
947 948
948 949 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
949 950 follows_repository = relationship('Repository', order_by='Repository.repo_name')
950 951
951 952
952 953 @classmethod
953 954 def get_repo_followers(cls, repo_id):
954 955 return Session.query(cls).filter(cls.follows_repo_id == repo_id)
955 956
956 957 class CacheInvalidation(Base, BaseModel):
957 958 __tablename__ = 'cache_invalidation'
958 959 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
959 960 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
960 961 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
961 962 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
962 963 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
963 964
964 965
965 966 def __init__(self, cache_key, cache_args=''):
966 967 self.cache_key = cache_key
967 968 self.cache_args = cache_args
968 969 self.cache_active = False
969 970
970 971 def __repr__(self):
971 972 return "<%s('%s:%s')>" % (self.__class__.__name__,
972 973 self.cache_id, self.cache_key)
973 974
974 975 class DbMigrateVersion(Base, BaseModel):
975 976 __tablename__ = 'db_migrate_version'
976 977 __table_args__ = {'extend_existing':True}
977 978 repository_id = Column('repository_id', String(250), primary_key=True)
978 979 repository_path = Column('repository_path', Text)
979 980 version = Column('version', Integer)
@@ -1,668 +1,681 b''
1 1 """ this is forms validation classes
2 2 http://formencode.org/module-formencode.validators.html
3 3 for list off all availible validators
4 4
5 5 we can create our own validators
6 6
7 7 The table below outlines the options which can be used in a schema in addition to the validators themselves
8 8 pre_validators [] These validators will be applied before the schema
9 9 chained_validators [] These validators will be applied after the schema
10 10 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
11 11 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
12 12 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
13 13 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
14 14
15 15
16 16 <name> = formencode.validators.<name of validator>
17 17 <name> must equal form name
18 18 list=[1,2,3,4,5]
19 19 for SELECT use formencode.All(OneOf(list), Int())
20 20
21 21 """
22 22 import os
23 23 import re
24 24 import logging
25 25 import traceback
26 26
27 27 import formencode
28 28 from formencode import All
29 29 from formencode.validators import UnicodeString, OneOf, Int, Number, Regex, \
30 30 Email, Bool, StringBoolean, Set
31 31
32 32 from pylons.i18n.translation import _
33 33 from webhelpers.pylonslib.secure_form import authentication_token
34 34
35 35 from rhodecode.lib.utils import repo_name_slug
36 36 from rhodecode.lib.auth import authenticate, get_crypt_password
37 37 from rhodecode.lib.exceptions import LdapImportError
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.db import User, UsersGroup, Group
41 41 from rhodecode import BACKENDS
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45 #this is needed to translate the messages using _() in validators
46 46 class State_obj(object):
47 47 _ = staticmethod(_)
48 48
49 49 #==============================================================================
50 50 # VALIDATORS
51 51 #==============================================================================
52 52 class ValidAuthToken(formencode.validators.FancyValidator):
53 53 messages = {'invalid_token':_('Token mismatch')}
54 54
55 55 def validate_python(self, value, state):
56 56
57 57 if value != authentication_token():
58 58 raise formencode.Invalid(self.message('invalid_token', state,
59 59 search_number=value), value, state)
60 60
61 61 def ValidUsername(edit, old_data):
62 62 class _ValidUsername(formencode.validators.FancyValidator):
63 63
64 64 def validate_python(self, value, state):
65 65 if value in ['default', 'new_user']:
66 66 raise formencode.Invalid(_('Invalid username'), value, state)
67 67 #check if user is unique
68 68 old_un = None
69 69 if edit:
70 70 old_un = UserModel().get(old_data.get('user_id')).username
71 71
72 72 if old_un != value or not edit:
73 73 if UserModel().get_by_username(value, cache=False,
74 74 case_insensitive=True):
75 75 raise formencode.Invalid(_('This username already '
76 76 'exists') , value, state)
77 77
78 78 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
79 79 raise formencode.Invalid(_('Username may only contain '
80 80 'alphanumeric characters '
81 81 'underscores, periods or dashes '
82 82 'and must begin with alphanumeric '
83 83 'character'), value, state)
84 84
85 85 return _ValidUsername
86 86
87 87
88 88 def ValidUsersGroup(edit, old_data):
89 89
90 90 class _ValidUsersGroup(formencode.validators.FancyValidator):
91 91
92 92 def validate_python(self, value, state):
93 93 if value in ['default']:
94 94 raise formencode.Invalid(_('Invalid group name'), value, state)
95 95 #check if group is unique
96 96 old_ugname = None
97 97 if edit:
98 98 old_ugname = UsersGroup.get(
99 99 old_data.get('users_group_id')).users_group_name
100 100
101 101 if old_ugname != value or not edit:
102 102 if UsersGroup.get_by_group_name(value, cache=False,
103 103 case_insensitive=True):
104 104 raise formencode.Invalid(_('This users group '
105 105 'already exists') , value,
106 106 state)
107 107
108 108
109 109 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
110 110 raise formencode.Invalid(_('Group name may only contain '
111 111 'alphanumeric characters '
112 112 'underscores, periods or dashes '
113 113 'and must begin with alphanumeric '
114 114 'character'), value, state)
115 115
116 116 return _ValidUsersGroup
117 117
118 118
119 119 def ValidReposGroup(edit, old_data):
120 120 class _ValidReposGroup(formencode.validators.FancyValidator):
121 121
122 122 def validate_python(self, value, state):
123 123 #TODO WRITE VALIDATIONS
124 124 group_name = value.get('group_name')
125 125 group_parent_id = int(value.get('group_parent_id') or - 1)
126 126
127 127 # slugify repo group just in case :)
128 128 slug = repo_name_slug(group_name)
129 129
130 130 # check for parent of self
131 131 if edit and old_data['group_id'] == group_parent_id:
132 132 e_dict = {'group_parent_id':_('Cannot assign this group '
133 133 'as parent')}
134 134 raise formencode.Invalid('', value, state,
135 135 error_dict=e_dict)
136 136
137 137 old_gname = None
138 138 if edit:
139 139 old_gname = Group.get(
140 140 old_data.get('group_id')).group_name
141 141
142 142 if old_gname != group_name or not edit:
143 143 # check filesystem
144 144 gr = Group.query().filter(Group.group_name == slug)\
145 145 .filter(Group.group_parent_id == group_parent_id).scalar()
146 146
147 147 if gr:
148 148 e_dict = {'group_name':_('This group already exists')}
149 149 raise formencode.Invalid('', value, state,
150 150 error_dict=e_dict)
151 151
152 152 return _ValidReposGroup
153 153
154 154 class ValidPassword(formencode.validators.FancyValidator):
155 155
156 156 def to_python(self, value, state):
157 157
158 158 if value:
159 159
160 160 if value.get('password'):
161 161 try:
162 162 value['password'] = get_crypt_password(value['password'])
163 163 except UnicodeEncodeError:
164 164 e_dict = {'password':_('Invalid characters in password')}
165 165 raise formencode.Invalid('', value, state, error_dict=e_dict)
166 166
167 167 if value.get('password_confirmation'):
168 168 try:
169 169 value['password_confirmation'] = \
170 170 get_crypt_password(value['password_confirmation'])
171 171 except UnicodeEncodeError:
172 172 e_dict = {'password_confirmation':_('Invalid characters in password')}
173 173 raise formencode.Invalid('', value, state, error_dict=e_dict)
174 174
175 175 if value.get('new_password'):
176 176 try:
177 177 value['new_password'] = \
178 178 get_crypt_password(value['new_password'])
179 179 except UnicodeEncodeError:
180 180 e_dict = {'new_password':_('Invalid characters in password')}
181 181 raise formencode.Invalid('', value, state, error_dict=e_dict)
182 182
183 183 return value
184 184
185 185 class ValidPasswordsMatch(formencode.validators.FancyValidator):
186 186
187 187 def validate_python(self, value, state):
188 188
189 189 if value['password'] != value['password_confirmation']:
190 190 e_dict = {'password_confirmation':
191 191 _('Passwords do not match')}
192 192 raise formencode.Invalid('', value, state, error_dict=e_dict)
193 193
194 194 class ValidAuth(formencode.validators.FancyValidator):
195 195 messages = {
196 196 'invalid_password':_('invalid password'),
197 197 'invalid_login':_('invalid user name'),
198 198 'disabled_account':_('Your account is disabled')
199 199
200 200 }
201 201 #error mapping
202 202 e_dict = {'username':messages['invalid_login'],
203 203 'password':messages['invalid_password']}
204 204 e_dict_disable = {'username':messages['disabled_account']}
205 205
206 206 def validate_python(self, value, state):
207 207 password = value['password']
208 208 username = value['username']
209 209 user = UserModel().get_by_username(username)
210 210
211 211 if authenticate(username, password):
212 212 return value
213 213 else:
214 214 if user and user.active is False:
215 215 log.warning('user %s is disabled', username)
216 216 raise formencode.Invalid(self.message('disabled_account',
217 217 state=State_obj),
218 218 value, state,
219 219 error_dict=self.e_dict_disable)
220 220 else:
221 221 log.warning('user %s not authenticated', username)
222 222 raise formencode.Invalid(self.message('invalid_password',
223 223 state=State_obj), value, state,
224 224 error_dict=self.e_dict)
225 225
226 226 class ValidRepoUser(formencode.validators.FancyValidator):
227 227
228 228 def to_python(self, value, state):
229 229 try:
230 230 User.query().filter(User.active == True)\
231 231 .filter(User.username == value).one()
232 232 except Exception:
233 233 raise formencode.Invalid(_('This username is not valid'),
234 234 value, state)
235 235 return value
236 236
237 237 def ValidRepoName(edit, old_data):
238 238 class _ValidRepoName(formencode.validators.FancyValidator):
239 239 def to_python(self, value, state):
240 240
241 241 repo_name = value.get('repo_name')
242 242
243 243 slug = repo_name_slug(repo_name)
244 244 if slug in ['_admin', '']:
245 245 e_dict = {'repo_name': _('This repository name is disallowed')}
246 246 raise formencode.Invalid('', value, state, error_dict=e_dict)
247 247
248 248
249 249 if value.get('repo_group'):
250 250 gr = Group.get(value.get('repo_group'))
251 251 group_path = gr.full_path
252 252 # value needs to be aware of group name in order to check
253 253 # db key This is an actuall just the name to store in the
254 254 # database
255 255 repo_name_full = group_path + Group.url_sep() + repo_name
256 256 else:
257 257 group_path = ''
258 258 repo_name_full = repo_name
259 259
260 260
261 261 value['repo_name_full'] = repo_name_full
262 262 if old_data.get('repo_name') != repo_name_full or not edit:
263 263
264 264 if group_path != '':
265 265 if RepoModel().get_by_repo_name(repo_name_full,):
266 266 e_dict = {'repo_name':_('This repository already '
267 267 'exists in group "%s"') %
268 268 gr.group_name}
269 269 raise formencode.Invalid('', value, state,
270 270 error_dict=e_dict)
271 271
272 272 else:
273 273 if RepoModel().get_by_repo_name(repo_name_full):
274 274 e_dict = {'repo_name':_('This repository '
275 275 'already exists')}
276 276 raise formencode.Invalid('', value, state,
277 277 error_dict=e_dict)
278 278 return value
279 279
280 280
281 281 return _ValidRepoName
282 282
283 283 def ValidForkName():
284 284 class _ValidForkName(formencode.validators.FancyValidator):
285 285 def to_python(self, value, state):
286
287 repo_name = value.get('fork_name')
288
289 slug = repo_name_slug(repo_name)
290 if slug in ['_admin', '']:
291 e_dict = {'repo_name': _('This repository name is disallowed')}
292 raise formencode.Invalid('', value, state, error_dict=e_dict)
293
294 if RepoModel().get_by_repo_name(repo_name):
295 e_dict = {'fork_name':_('This repository '
296 'already exists')}
297 raise formencode.Invalid('', value, state,
298 error_dict=e_dict)
286 299 return value
287 300 return _ValidForkName
288 301
289 302
290 303 def SlugifyName():
291 304 class _SlugifyName(formencode.validators.FancyValidator):
292 305
293 306 def to_python(self, value, state):
294 307 return repo_name_slug(value)
295 308
296 309 return _SlugifyName
297 310
298 311 def ValidCloneUri():
299 312 from mercurial.httprepo import httprepository, httpsrepository
300 313 from rhodecode.lib.utils import make_ui
301 314
302 315 class _ValidCloneUri(formencode.validators.FancyValidator):
303 316
304 317 def to_python(self, value, state):
305 318 if not value:
306 319 pass
307 320 elif value.startswith('https'):
308 321 try:
309 322 httpsrepository(make_ui('db'), value).capabilities
310 323 except Exception, e:
311 324 log.error(traceback.format_exc())
312 325 raise formencode.Invalid(_('invalid clone url'), value,
313 326 state)
314 327 elif value.startswith('http'):
315 328 try:
316 329 httprepository(make_ui('db'), value).capabilities
317 330 except Exception, e:
318 331 log.error(traceback.format_exc())
319 332 raise formencode.Invalid(_('invalid clone url'), value,
320 333 state)
321 334 else:
322 335 raise formencode.Invalid(_('Invalid clone url, provide a '
323 336 'valid clone http\s url'), value,
324 337 state)
325 338 return value
326 339
327 340 return _ValidCloneUri
328 341
329 342 def ValidForkType(old_data):
330 343 class _ValidForkType(formencode.validators.FancyValidator):
331 344
332 345 def to_python(self, value, state):
333 346 if old_data['repo_type'] != value:
334 347 raise formencode.Invalid(_('Fork have to be the same '
335 348 'type as original'), value, state)
336 349
337 350 return value
338 351 return _ValidForkType
339 352
340 353 class ValidPerms(formencode.validators.FancyValidator):
341 354 messages = {'perm_new_member_name':_('This username or users group name'
342 355 ' is not valid')}
343 356
344 357 def to_python(self, value, state):
345 358 perms_update = []
346 359 perms_new = []
347 360 #build a list of permission to update and new permission to create
348 361 for k, v in value.items():
349 362 #means new added member to permissions
350 363 if k.startswith('perm_new_member'):
351 364 new_perm = value.get('perm_new_member', False)
352 365 new_member = value.get('perm_new_member_name', False)
353 366 new_type = value.get('perm_new_member_type')
354 367
355 368 if new_member and new_perm:
356 369 if (new_member, new_perm, new_type) not in perms_new:
357 370 perms_new.append((new_member, new_perm, new_type))
358 371 elif k.startswith('u_perm_') or k.startswith('g_perm_'):
359 372 member = k[7:]
360 373 t = {'u':'user',
361 374 'g':'users_group'}[k[0]]
362 375 if member == 'default':
363 376 if value['private']:
364 377 #set none for default when updating to private repo
365 378 v = 'repository.none'
366 379 perms_update.append((member, v, t))
367 380
368 381 value['perms_updates'] = perms_update
369 382 value['perms_new'] = perms_new
370 383
371 384 #update permissions
372 385 for k, v, t in perms_new:
373 386 try:
374 387 if t is 'user':
375 388 self.user_db = User.query()\
376 389 .filter(User.active == True)\
377 390 .filter(User.username == k).one()
378 391 if t is 'users_group':
379 392 self.user_db = UsersGroup.query()\
380 393 .filter(UsersGroup.users_group_active == True)\
381 394 .filter(UsersGroup.users_group_name == k).one()
382 395
383 396 except Exception:
384 397 msg = self.message('perm_new_member_name',
385 398 state=State_obj)
386 399 raise formencode.Invalid(msg, value, state,
387 400 error_dict={'perm_new_member_name':msg})
388 401 return value
389 402
390 403 class ValidSettings(formencode.validators.FancyValidator):
391 404
392 405 def to_python(self, value, state):
393 406 #settings form can't edit user
394 407 if value.has_key('user'):
395 408 del['value']['user']
396 409
397 410 return value
398 411
399 412 class ValidPath(formencode.validators.FancyValidator):
400 413 def to_python(self, value, state):
401 414
402 415 if not os.path.isdir(value):
403 416 msg = _('This is not a valid path')
404 417 raise formencode.Invalid(msg, value, state,
405 418 error_dict={'paths_root_path':msg})
406 419 return value
407 420
408 421 def UniqSystemEmail(old_data):
409 422 class _UniqSystemEmail(formencode.validators.FancyValidator):
410 423 def to_python(self, value, state):
411 424 value = value.lower()
412 425 if old_data.get('email') != value:
413 426 user = User.query().filter(User.email == value).scalar()
414 427 if user:
415 428 raise formencode.Invalid(
416 429 _("This e-mail address is already taken"),
417 430 value, state)
418 431 return value
419 432
420 433 return _UniqSystemEmail
421 434
422 435 class ValidSystemEmail(formencode.validators.FancyValidator):
423 436 def to_python(self, value, state):
424 437 value = value.lower()
425 438 user = User.query().filter(User.email == value).scalar()
426 439 if user is None:
427 440 raise formencode.Invalid(_("This e-mail address doesn't exist.") ,
428 441 value, state)
429 442
430 443 return value
431 444
432 445 class LdapLibValidator(formencode.validators.FancyValidator):
433 446
434 447 def to_python(self, value, state):
435 448
436 449 try:
437 450 import ldap
438 451 except ImportError:
439 452 raise LdapImportError
440 453 return value
441 454
442 455 class AttrLoginValidator(formencode.validators.FancyValidator):
443 456
444 457 def to_python(self, value, state):
445 458
446 459 if not value or not isinstance(value, (str, unicode)):
447 460 raise formencode.Invalid(_("The LDAP Login attribute of the CN "
448 461 "must be specified - this is the name "
449 462 "of the attribute that is equivalent "
450 463 "to 'username'"),
451 464 value, state)
452 465
453 466 return value
454 467
455 468 #===============================================================================
456 469 # FORMS
457 470 #===============================================================================
458 471 class LoginForm(formencode.Schema):
459 472 allow_extra_fields = True
460 473 filter_extra_fields = True
461 474 username = UnicodeString(
462 475 strip=True,
463 476 min=1,
464 477 not_empty=True,
465 478 messages={
466 479 'empty':_('Please enter a login'),
467 480 'tooShort':_('Enter a value %(min)i characters long or more')}
468 481 )
469 482
470 483 password = UnicodeString(
471 484 strip=True,
472 485 min=3,
473 486 not_empty=True,
474 487 messages={
475 488 'empty':_('Please enter a password'),
476 489 'tooShort':_('Enter %(min)i characters or more')}
477 490 )
478 491
479 492
480 493 #chained validators have access to all data
481 494 chained_validators = [ValidAuth]
482 495
483 496 def UserForm(edit=False, old_data={}):
484 497 class _UserForm(formencode.Schema):
485 498 allow_extra_fields = True
486 499 filter_extra_fields = True
487 500 username = All(UnicodeString(strip=True, min=1, not_empty=True),
488 501 ValidUsername(edit, old_data))
489 502 if edit:
490 503 new_password = All(UnicodeString(strip=True, min=6, not_empty=False))
491 504 admin = StringBoolean(if_missing=False)
492 505 else:
493 506 password = All(UnicodeString(strip=True, min=6, not_empty=True))
494 507 active = StringBoolean(if_missing=False)
495 508 name = UnicodeString(strip=True, min=1, not_empty=True)
496 509 lastname = UnicodeString(strip=True, min=1, not_empty=True)
497 510 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
498 511
499 512 chained_validators = [ValidPassword]
500 513
501 514 return _UserForm
502 515
503 516
504 517 def UsersGroupForm(edit=False, old_data={}, available_members=[]):
505 518 class _UsersGroupForm(formencode.Schema):
506 519 allow_extra_fields = True
507 520 filter_extra_fields = True
508 521
509 522 users_group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
510 523 ValidUsersGroup(edit, old_data))
511 524
512 525 users_group_active = StringBoolean(if_missing=False)
513 526
514 527 if edit:
515 528 users_group_members = OneOf(available_members, hideList=False,
516 529 testValueList=True,
517 530 if_missing=None, not_empty=False)
518 531
519 532 return _UsersGroupForm
520 533
521 534 def ReposGroupForm(edit=False, old_data={}, available_groups=[]):
522 535 class _ReposGroupForm(formencode.Schema):
523 536 allow_extra_fields = True
524 537 filter_extra_fields = True
525 538
526 539 group_name = All(UnicodeString(strip=True, min=1, not_empty=True),
527 540 SlugifyName())
528 541 group_description = UnicodeString(strip=True, min=1,
529 542 not_empty=True)
530 543 group_parent_id = OneOf(available_groups, hideList=False,
531 544 testValueList=True,
532 545 if_missing=None, not_empty=False)
533 546
534 547 chained_validators = [ValidReposGroup(edit, old_data)]
535 548
536 549 return _ReposGroupForm
537 550
538 551 def RegisterForm(edit=False, old_data={}):
539 552 class _RegisterForm(formencode.Schema):
540 553 allow_extra_fields = True
541 554 filter_extra_fields = True
542 555 username = All(ValidUsername(edit, old_data),
543 556 UnicodeString(strip=True, min=1, not_empty=True))
544 557 password = All(UnicodeString(strip=True, min=6, not_empty=True))
545 558 password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True))
546 559 active = StringBoolean(if_missing=False)
547 560 name = UnicodeString(strip=True, min=1, not_empty=True)
548 561 lastname = UnicodeString(strip=True, min=1, not_empty=True)
549 562 email = All(Email(not_empty=True), UniqSystemEmail(old_data))
550 563
551 564 chained_validators = [ValidPasswordsMatch, ValidPassword]
552 565
553 566 return _RegisterForm
554 567
555 568 def PasswordResetForm():
556 569 class _PasswordResetForm(formencode.Schema):
557 570 allow_extra_fields = True
558 571 filter_extra_fields = True
559 572 email = All(ValidSystemEmail(), Email(not_empty=True))
560 573 return _PasswordResetForm
561 574
562 575 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
563 576 repo_groups=[]):
564 577 class _RepoForm(formencode.Schema):
565 578 allow_extra_fields = True
566 579 filter_extra_fields = False
567 580 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
568 581 SlugifyName())
569 582 clone_uri = All(UnicodeString(strip=True, min=1, not_empty=False),
570 583 ValidCloneUri()())
571 584 repo_group = OneOf(repo_groups, hideList=True)
572 585 repo_type = OneOf(supported_backends)
573 586 description = UnicodeString(strip=True, min=1, not_empty=True)
574 587 private = StringBoolean(if_missing=False)
575 588 enable_statistics = StringBoolean(if_missing=False)
576 589 enable_downloads = StringBoolean(if_missing=False)
577 590
578 591 if edit:
579 592 #this is repo owner
580 593 user = All(UnicodeString(not_empty=True), ValidRepoUser)
581 594
582 595 chained_validators = [ValidRepoName(edit, old_data), ValidPerms]
583 596 return _RepoForm
584 597
585 598 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
586 599 class _RepoForkForm(formencode.Schema):
587 600 allow_extra_fields = True
588 601 filter_extra_fields = False
589 602 fork_name = All(UnicodeString(strip=True, min=1, not_empty=True),
590 603 SlugifyName())
591 604 description = UnicodeString(strip=True, min=1, not_empty=True)
592 605 private = StringBoolean(if_missing=False)
593 606 repo_type = All(ValidForkType(old_data), OneOf(supported_backends))
594 607
595 608 chained_validators = [ValidForkName()]
596 609
597 610 return _RepoForkForm
598 611
599 612 def RepoSettingsForm(edit=False, old_data={}):
600 613 class _RepoForm(formencode.Schema):
601 614 allow_extra_fields = True
602 615 filter_extra_fields = False
603 616 repo_name = All(UnicodeString(strip=True, min=1, not_empty=True),
604 617 SlugifyName())
605 618 description = UnicodeString(strip=True, min=1, not_empty=True)
606 619 private = StringBoolean(if_missing=False)
607 620
608 621 chained_validators = [ValidRepoName(edit, old_data), ValidPerms, ValidSettings]
609 622 return _RepoForm
610 623
611 624
612 625 def ApplicationSettingsForm():
613 626 class _ApplicationSettingsForm(formencode.Schema):
614 627 allow_extra_fields = True
615 628 filter_extra_fields = False
616 629 rhodecode_title = UnicodeString(strip=True, min=1, not_empty=True)
617 630 rhodecode_realm = UnicodeString(strip=True, min=1, not_empty=True)
618 631 rhodecode_ga_code = UnicodeString(strip=True, min=1, not_empty=False)
619 632
620 633 return _ApplicationSettingsForm
621 634
622 635 def ApplicationUiSettingsForm():
623 636 class _ApplicationUiSettingsForm(formencode.Schema):
624 637 allow_extra_fields = True
625 638 filter_extra_fields = False
626 639 web_push_ssl = OneOf(['true', 'false'], if_missing='false')
627 640 paths_root_path = All(ValidPath(), UnicodeString(strip=True, min=1, not_empty=True))
628 641 hooks_changegroup_update = OneOf(['True', 'False'], if_missing=False)
629 642 hooks_changegroup_repo_size = OneOf(['True', 'False'], if_missing=False)
630 643 hooks_pretxnchangegroup_push_logger = OneOf(['True', 'False'], if_missing=False)
631 644 hooks_preoutgoing_pull_logger = OneOf(['True', 'False'], if_missing=False)
632 645
633 646 return _ApplicationUiSettingsForm
634 647
635 648 def DefaultPermissionsForm(perms_choices, register_choices, create_choices):
636 649 class _DefaultPermissionsForm(formencode.Schema):
637 650 allow_extra_fields = True
638 651 filter_extra_fields = True
639 652 overwrite_default = StringBoolean(if_missing=False)
640 653 anonymous = OneOf(['True', 'False'], if_missing=False)
641 654 default_perm = OneOf(perms_choices)
642 655 default_register = OneOf(register_choices)
643 656 default_create = OneOf(create_choices)
644 657
645 658 return _DefaultPermissionsForm
646 659
647 660
648 661 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices, tls_kind_choices):
649 662 class _LdapSettingsForm(formencode.Schema):
650 663 allow_extra_fields = True
651 664 filter_extra_fields = True
652 665 pre_validators = [LdapLibValidator]
653 666 ldap_active = StringBoolean(if_missing=False)
654 667 ldap_host = UnicodeString(strip=True,)
655 668 ldap_port = Number(strip=True,)
656 669 ldap_tls_kind = OneOf(tls_kind_choices)
657 670 ldap_tls_reqcert = OneOf(tls_reqcert_choices)
658 671 ldap_dn_user = UnicodeString(strip=True,)
659 672 ldap_dn_pass = UnicodeString(strip=True,)
660 673 ldap_base_dn = UnicodeString(strip=True,)
661 674 ldap_filter = UnicodeString(strip=True,)
662 675 ldap_search_scope = OneOf(search_scope_choices)
663 676 ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
664 677 ldap_attr_firstname = UnicodeString(strip=True,)
665 678 ldap_attr_lastname = UnicodeString(strip=True,)
666 679 ldap_attr_email = UnicodeString(strip=True,)
667 680
668 681 return _LdapSettingsForm
@@ -1,148 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.tests.test_crawer
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Test for crawling a project for memory usage
7 7 This should be runned just as regular script together
8 8 with a watch script that will show memory usage.
9 9
10 10 watch -n1 ./rhodecode/tests/mem_watch
11 11
12 12 :created_on: Apr 21, 2010
13 13 :author: marcink
14 14 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
15 15 :license: GPLv3, see COPYING for more details.
16 16 """
17 17 # This program is free software: you can redistribute it and/or modify
18 18 # it under the terms of the GNU General Public License as published by
19 19 # the Free Software Foundation, either version 3 of the License, or
20 20 # (at your option) any later version.
21 21 #
22 22 # This program is distributed in the hope that it will be useful,
23 23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 25 # GNU General Public License for more details.
26 26 #
27 27 # You should have received a copy of the GNU General Public License
28 28 # along with this program. If not, see <http://www.gnu.org/licenses/>.
29 29
30 30
31 31 import cookielib
32 32 import urllib
33 33 import urllib2
34 34 import vcs
35 35 import time
36 36
37 37 from os.path import join as jn
38 38
39 39
40 40 BASE_URI = 'http://127.0.0.1:5000/%s'
41 41 PROJECT = 'CPython'
42 42 PROJECT_PATH = jn('/', 'home', 'marcink', 'hg_repos')
43 43
44 44
45 45 cj = cookielib.FileCookieJar('/tmp/rc_test_cookie.txt')
46 46 o = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
47 47 o.addheaders = [
48 48 ('User-agent', 'rhodecode-crawler'),
49 49 ('Accept-Language', 'en - us, en;q = 0.5')
50 50 ]
51 51
52 52 urllib2.install_opener(o)
53 53
54 54
55 55
56 56 def test_changelog_walk(pages=100):
57 57 total_time = 0
58 58 for i in range(1, pages):
59 59
60 60 page = '/'.join((PROJECT, 'changelog',))
61 61
62 62 full_uri = (BASE_URI % page) + '?' + urllib.urlencode({'page':i})
63 63 s = time.time()
64 64 f = o.open(full_uri)
65 65 size = len(f.read())
66 66 e = time.time() - s
67 67 total_time += e
68 68 print 'visited %s size:%s req:%s ms' % (full_uri, size, e)
69 69
70 70
71 71 print 'total_time', total_time
72 72 print 'average on req', total_time / float(pages)
73 73
74 74
75 75 def test_changeset_walk(limit=None):
76 76 print 'processing', jn(PROJECT_PATH, PROJECT)
77 77 total_time = 0
78 78
79 79 repo = vcs.get_repo(jn(PROJECT_PATH, PROJECT))
80 80 cnt = 0
81 81 for i in repo:
82 82 cnt += 1
83 83 raw_cs = '/'.join((PROJECT, 'changeset', i.raw_id))
84 84 if limit and limit == cnt:
85 85 break
86 86
87 87 full_uri = (BASE_URI % raw_cs)
88 88 s = time.time()
89 89 f = o.open(full_uri)
90 90 size = len(f.read())
91 91 e = time.time() - s
92 92 total_time += e
93 93 print '%s visited %s\%s size:%s req:%s ms' % (cnt, full_uri, i, size, e)
94 94
95 95 print 'total_time', total_time
96 96 print 'average on req', total_time / float(cnt)
97 97
98 98
99 99 def test_files_walk(limit=100):
100 100 print 'processing', jn(PROJECT_PATH, PROJECT)
101 101 total_time = 0
102 102
103 103 repo = vcs.get_repo(jn(PROJECT_PATH, PROJECT))
104 104
105 from rhodecode.lib.oset import OrderedSet
105 from rhodecode.lib.compat import OrderedSet
106 106
107 107 paths_ = OrderedSet([''])
108 108 try:
109 109 tip = repo.get_changeset('tip')
110 110 for topnode, dirs, files in tip.walk('/'):
111 111
112 112 for dir in dirs:
113 113 paths_.add(dir.path)
114 114 for f in dir:
115 115 paths_.add(f.path)
116 116
117 117 for f in files:
118 118 paths_.add(f.path)
119 119
120 120 except vcs.exception.RepositoryError, e:
121 121 pass
122 122
123 123 cnt = 0
124 124 for f in paths_:
125 125 cnt += 1
126 126 if limit and limit == cnt:
127 127 break
128 128
129 129 file_path = '/'.join((PROJECT, 'files', 'tip', f))
130 130
131 131 full_uri = (BASE_URI % file_path)
132 132 s = time.time()
133 133 f = o.open(full_uri)
134 134 size = len(f.read())
135 135 e = time.time() - s
136 136 total_time += e
137 137 print '%s visited %s size:%s req:%s ms' % (cnt, full_uri, size, e)
138 138
139 139 print 'total_time', total_time
140 140 print 'average on req', total_time / float(cnt)
141 141
142 142
143 143
144 144 test_changelog_walk(40)
145 145 time.sleep(2)
146 146 test_changeset_walk(limit=100)
147 147 time.sleep(2)
148 148 test_files_walk(100)
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now