##// END OF EJS Templates
Implementes #509 require SSL flag now works for both git and mercurial....
marcink -
r2668:f0851f37 beta
parent child Browse files
Show More
@@ -1,236 +1,251 b''
1 1 """The base Controller API
2 2
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 6 import time
7 7 import traceback
8 8
9 9 from paste.auth.basic import AuthBasicAuthenticator
10 10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 11 from paste.httpheaders import WWW_AUTHENTICATE
12 12
13 13 from pylons import config, tmpl_context as c, request, session, url
14 14 from pylons.controllers import WSGIController
15 15 from pylons.controllers.util import redirect
16 16 from pylons.templating import render_mako as render
17 17
18 18 from rhodecode import __version__, BACKENDS
19 19
20 20 from rhodecode.lib.utils2 import str2bool, safe_unicode
21 21 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
22 22 HasPermissionAnyMiddleware, CookieStoreWrapper
23 23 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
24 24 from rhodecode.model import meta
25 25
26 from rhodecode.model.db import Repository
26 from rhodecode.model.db import Repository, RhodeCodeUi
27 27 from rhodecode.model.notification import NotificationModel
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 def _get_ip_addr(environ):
34 34 proxy_key = 'HTTP_X_REAL_IP'
35 35 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
36 36 def_key = 'REMOTE_ADDR'
37 37
38 38 ip = environ.get(proxy_key2)
39 39 if ip:
40 40 return ip
41 41
42 42 ip = environ.get(proxy_key)
43 43
44 44 if ip:
45 45 return ip
46 46
47 47 ip = environ.get(def_key, '0.0.0.0')
48 48 return ip
49 49
50 50
51 51 def _get_access_path(environ):
52 52 path = environ.get('PATH_INFO')
53 53 org_req = environ.get('pylons.original_request')
54 54 if org_req:
55 55 path = org_req.environ.get('PATH_INFO')
56 56 return path
57 57
58 58
59 59 class BasicAuth(AuthBasicAuthenticator):
60 60
61 61 def __init__(self, realm, authfunc, auth_http_code=None):
62 62 self.realm = realm
63 63 self.authfunc = authfunc
64 64 self._rc_auth_http_code = auth_http_code
65 65
66 66 def build_authentication(self):
67 67 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
68 68 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
69 69 # return 403 if alternative http return code is specified in
70 70 # RhodeCode config
71 71 return HTTPForbidden(headers=head)
72 72 return HTTPUnauthorized(headers=head)
73 73
74 74
75 75 class BaseVCSController(object):
76 76
77 77 def __init__(self, application, config):
78 78 self.application = application
79 79 self.config = config
80 80 # base path of repo locations
81 81 self.basepath = self.config['base_path']
82 82 #authenticate this mercurial request using authfunc
83 83 self.authenticate = BasicAuth('', authfunc,
84 84 config.get('auth_ret_code'))
85 85 self.ipaddr = '0.0.0.0'
86 86
87 87 def _handle_request(self, environ, start_response):
88 88 raise NotImplementedError()
89 89
90 90 def _get_by_id(self, repo_name):
91 91 """
92 92 Get's a special pattern _<ID> from clone url and tries to replace it
93 93 with a repository_name for support of _<ID> non changable urls
94 94
95 95 :param repo_name:
96 96 """
97 97 try:
98 98 data = repo_name.split('/')
99 99 if len(data) >= 2:
100 100 by_id = data[1].split('_')
101 101 if len(by_id) == 2 and by_id[1].isdigit():
102 102 _repo_name = Repository.get(by_id[1]).repo_name
103 103 data[1] = _repo_name
104 104 except:
105 105 log.debug('Failed to extract repo_name from id %s' % (
106 106 traceback.format_exc()
107 107 )
108 108 )
109 109
110 110 return '/'.join(data)
111 111
112 112 def _invalidate_cache(self, repo_name):
113 113 """
114 114 Set's cache for this repository for invalidation on next access
115 115
116 116 :param repo_name: full repo name, also a cache key
117 117 """
118 118 invalidate_cache('get_repo_cached_%s' % repo_name)
119 119
120 120 def _check_permission(self, action, user, repo_name):
121 121 """
122 122 Checks permissions using action (push/pull) user and repository
123 123 name
124 124
125 125 :param action: push or pull action
126 126 :param user: user instance
127 127 :param repo_name: repository name
128 128 """
129 129 if action == 'push':
130 130 if not HasPermissionAnyMiddleware('repository.write',
131 131 'repository.admin')(user,
132 132 repo_name):
133 133 return False
134 134
135 135 else:
136 136 #any other action need at least read permission
137 137 if not HasPermissionAnyMiddleware('repository.read',
138 138 'repository.write',
139 139 'repository.admin')(user,
140 140 repo_name):
141 141 return False
142 142
143 143 return True
144 144
145 145 def _get_ip_addr(self, environ):
146 146 return _get_ip_addr(environ)
147 147
148 def _check_ssl(self, environ, start_response):
149 """
150 Checks the SSL check flag and returns False if SSL is not present
151 and required True otherwise
152 """
153 org_proto = environ['wsgi._org_proto']
154 #check if we have SSL required ! if not it's a bad request !
155 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl')\
156 .scalar().ui_value)
157 if require_ssl and org_proto == 'http':
158 log.debug('proto is %s and SSL is required BAD REQUEST !'
159 % org_proto)
160 return False
161 return True
162
148 163 def __call__(self, environ, start_response):
149 164 start = time.time()
150 165 try:
151 166 return self._handle_request(environ, start_response)
152 167 finally:
153 168 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
154 169 log.debug('Request time: %.3fs' % (time.time() - start))
155 170 meta.Session.remove()
156 171
157 172
158 173 class BaseController(WSGIController):
159 174
160 175 def __before__(self):
161 176 c.rhodecode_version = __version__
162 177 c.rhodecode_instanceid = config.get('instance_id')
163 178 c.rhodecode_name = config.get('rhodecode_title')
164 179 c.use_gravatar = str2bool(config.get('use_gravatar'))
165 180 c.ga_code = config.get('rhodecode_ga_code')
166 181 c.repo_name = get_repo_slug(request)
167 182 c.backends = BACKENDS.keys()
168 183 c.unread_notifications = NotificationModel()\
169 184 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
170 185 self.cut_off_limit = int(config.get('cut_off_limit'))
171 186
172 187 self.sa = meta.Session
173 188 self.scm_model = ScmModel(self.sa)
174 189 self.ip_addr = ''
175 190
176 191 def __call__(self, environ, start_response):
177 192 """Invoke the Controller"""
178 193 # WSGIController.__call__ dispatches to the Controller method
179 194 # the request is routed to. This routing information is
180 195 # available in environ['pylons.routes_dict']
181 196 start = time.time()
182 197 try:
183 198 self.ip_addr = _get_ip_addr(environ)
184 199 # make sure that we update permissions each time we call controller
185 200 api_key = request.GET.get('api_key')
186 201 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
187 202 user_id = cookie_store.get('user_id', None)
188 203 username = get_container_username(environ, config)
189 204 auth_user = AuthUser(user_id, api_key, username)
190 205 request.user = auth_user
191 206 self.rhodecode_user = c.rhodecode_user = auth_user
192 207 if not self.rhodecode_user.is_authenticated and \
193 208 self.rhodecode_user.user_id is not None:
194 209 self.rhodecode_user.set_authenticated(
195 210 cookie_store.get('is_authenticated')
196 211 )
197 212 log.info('IP: %s User: %s accessed %s' % (
198 213 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
199 214 )
200 215 return WSGIController.__call__(self, environ, start_response)
201 216 finally:
202 217 log.info('IP: %s Request to %s time: %.3fs' % (
203 218 _get_ip_addr(environ),
204 219 safe_unicode(_get_access_path(environ)), time.time() - start)
205 220 )
206 221 meta.Session.remove()
207 222
208 223
209 224 class BaseRepoController(BaseController):
210 225 """
211 226 Base class for controllers responsible for loading all needed data for
212 227 repository loaded items are
213 228
214 229 c.rhodecode_repo: instance of scm repository
215 230 c.rhodecode_db_repo: instance of db
216 231 c.repository_followers: number of followers
217 232 c.repository_forks: number of forks
218 233 """
219 234
220 235 def __before__(self):
221 236 super(BaseRepoController, self).__before__()
222 237 if c.repo_name:
223 238
224 239 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
225 240 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
226 241
227 242 if c.rhodecode_repo is None:
228 243 log.error('%s this repository is present in database but it '
229 244 'cannot be created as an scm instance', c.repo_name)
230 245
231 246 redirect(url('home'))
232 247
233 248 # some globals counter for menu
234 249 c.repository_followers = self.scm_model.get_followers(dbr)
235 250 c.repository_forks = self.scm_model.get_forks(dbr)
236 251 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
@@ -1,62 +1,61 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.https_fixup
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 middleware to handle https correctly
7 7
8 8 :created_on: May 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 from rhodecode.lib.utils2 import str2bool
27 27
28 28
29 29 class HttpsFixup(object):
30 30
31 31 def __init__(self, app, config):
32 32 self.application = app
33 33 self.config = config
34 34
35 35 def __call__(self, environ, start_response):
36 36 self.__fixup(environ)
37 37 return self.application(environ, start_response)
38 38
39 39 def __fixup(self, environ):
40 40 """
41 41 Function to fixup the environ as needed. In order to use this
42 42 middleware you should set this header inside your
43 43 proxy ie. nginx, apache etc.
44 44 """
45
46 if str2bool(self.config.get('force_https')):
47 proto = 'https'
48 else:
45 # DETECT PROTOCOL !
49 46 if 'HTTP_X_URL_SCHEME' in environ:
50 47 proto = environ.get('HTTP_X_URL_SCHEME')
51 48 elif 'HTTP_X_FORWARDED_SCHEME' in environ:
52 49 proto = environ.get('HTTP_X_FORWARDED_SCHEME')
53 50 elif 'HTTP_X_FORWARDED_PROTO' in environ:
54 51 proto = environ.get('HTTP_X_FORWARDED_PROTO')
55 52 else:
56 53 proto = 'http'
57 if proto == 'https':
54 org_proto = proto
55
56 # if we have force, just override
57 if str2bool(self.config.get('force_https')):
58 proto = 'https'
59
58 60 environ['wsgi.url_scheme'] = proto
59 else:
60 environ['wsgi.url_scheme'] = 'http'
61
62 return None
61 environ['wsgi._org_proto'] = org_proto
@@ -1,305 +1,306 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33 from dulwich.web import LimitedInputFilter, GunzipFilter
34 34
35 35
36 36 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
37 37
38 38 def handle(self):
39 39 write = lambda x: self.proto.write_sideband(1, x)
40 40
41 41 graph_walker = dulserver.ProtocolGraphWalker(self,
42 42 self.repo.object_store,
43 43 self.repo.get_peeled)
44 44 objects_iter = self.repo.fetch_objects(
45 45 graph_walker.determine_wants, graph_walker, self.progress,
46 46 get_tagged=self.get_tagged)
47 47
48 48 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
49 49 # that the client still expects a 0-object pack in most cases.
50 50 if objects_iter is None:
51 51 return
52 52
53 53 self.progress("counting objects: %d, done.\n" % len(objects_iter))
54 54 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
55 55 objects_iter)
56 56 messages = []
57 57 messages.append('thank you for using rhodecode')
58 58
59 59 for msg in messages:
60 60 self.progress(msg + "\n")
61 61 # we are done
62 62 self.proto.write("0000")
63 63
64 64
65 65 dulserver.DEFAULT_HANDLERS = {
66 66 #git-ls-remote, git-clone, git-fetch and git-pull
67 67 'git-upload-pack': SimpleGitUploadPackHandler,
68 68 #git-push
69 69 'git-receive-pack': dulserver.ReceivePackHandler,
70 70 }
71 71
72 72 # not used for now until dulwich get's fixed
73 73 #from dulwich.repo import Repo
74 74 #from dulwich.web import make_wsgi_chain
75 75
76 76 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
77 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
78 HTTPBadRequest, HTTPNotAcceptable
77 79
78 80 from rhodecode.lib.utils2 import safe_str
79 81 from rhodecode.lib.base import BaseVCSController
80 82 from rhodecode.lib.auth import get_container_username
81 83 from rhodecode.lib.utils import is_valid_repo, make_ui
82 84 from rhodecode.model.db import User, RhodeCodeUi
83 85
84 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
85
86 86 log = logging.getLogger(__name__)
87 87
88 88
89 89 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
90 90
91 91
92 92 def is_git(environ):
93 93 path_info = environ['PATH_INFO']
94 94 isgit_path = GIT_PROTO_PAT.match(path_info)
95 95 log.debug('pathinfo: %s detected as GIT %s' % (
96 96 path_info, isgit_path != None)
97 97 )
98 98 return isgit_path
99 99
100 100
101 101 class SimpleGit(BaseVCSController):
102 102
103 103 def _handle_request(self, environ, start_response):
104 104
105 105 if not is_git(environ):
106 106 return self.application(environ, start_response)
107
107 if not self._check_ssl(environ, start_response):
108 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
108 109 ipaddr = self._get_ip_addr(environ)
109 110 username = None
110 111 self._git_first_op = False
111 112 # skip passing error to error controller
112 113 environ['pylons.status_code_redirect'] = True
113 114
114 115 #======================================================================
115 116 # EXTRACT REPOSITORY NAME FROM ENV
116 117 #======================================================================
117 118 try:
118 119 repo_name = self.__get_repository(environ)
119 120 log.debug('Extracted repo name is %s' % repo_name)
120 121 except:
121 122 return HTTPInternalServerError()(environ, start_response)
122 123
123 124 # quick check if that dir exists...
124 125 if is_valid_repo(repo_name, self.basepath) is False:
125 126 return HTTPNotFound()(environ, start_response)
126 127
127 128 #======================================================================
128 129 # GET ACTION PULL or PUSH
129 130 #======================================================================
130 131 action = self.__get_action(environ)
131 132
132 133 #======================================================================
133 134 # CHECK ANONYMOUS PERMISSION
134 135 #======================================================================
135 136 if action in ['pull', 'push']:
136 137 anonymous_user = self.__get_user('default')
137 138 username = anonymous_user.username
138 139 anonymous_perm = self._check_permission(action, anonymous_user,
139 140 repo_name)
140 141
141 142 if anonymous_perm is not True or anonymous_user.active is False:
142 143 if anonymous_perm is not True:
143 144 log.debug('Not enough credentials to access this '
144 145 'repository as anonymous user')
145 146 if anonymous_user.active is False:
146 147 log.debug('Anonymous access is disabled, running '
147 148 'authentication')
148 149 #==============================================================
149 150 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
150 151 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
151 152 #==============================================================
152 153
153 154 # Attempting to retrieve username from the container
154 155 username = get_container_username(environ, self.config)
155 156
156 157 # If not authenticated by the container, running basic auth
157 158 if not username:
158 159 self.authenticate.realm = \
159 160 safe_str(self.config['rhodecode_realm'])
160 161 result = self.authenticate(environ)
161 162 if isinstance(result, str):
162 163 AUTH_TYPE.update(environ, 'basic')
163 164 REMOTE_USER.update(environ, result)
164 165 username = result
165 166 else:
166 167 return result.wsgi_application(environ, start_response)
167 168
168 169 #==============================================================
169 170 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
170 171 #==============================================================
171 172 try:
172 173 user = self.__get_user(username)
173 174 if user is None or not user.active:
174 175 return HTTPForbidden()(environ, start_response)
175 176 username = user.username
176 177 except:
177 178 log.error(traceback.format_exc())
178 179 return HTTPInternalServerError()(environ, start_response)
179 180
180 181 #check permissions for this repository
181 182 perm = self._check_permission(action, user, repo_name)
182 183 if perm is not True:
183 184 return HTTPForbidden()(environ, start_response)
184 185
185 186 extras = {
186 187 'ip': ipaddr,
187 188 'username': username,
188 189 'action': action,
189 190 'repository': repo_name,
190 191 'scm': 'git',
191 192 }
192 193
193 194 #===================================================================
194 195 # GIT REQUEST HANDLING
195 196 #===================================================================
196 197 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
197 198 log.debug('Repository path is %s' % repo_path)
198 199
199 200 baseui = make_ui('db')
200 201 self.__inject_extras(repo_path, baseui, extras)
201 202
202 203 try:
203 204 # invalidate cache on push
204 205 if action == 'push':
205 206 self._invalidate_cache(repo_name)
206 207 self._handle_githooks(repo_name, action, baseui, environ)
207 208
208 209 log.info('%s action on GIT repo "%s"' % (action, repo_name))
209 210 app = self.__make_app(repo_name, repo_path, username)
210 211 return app(environ, start_response)
211 212 except Exception:
212 213 log.error(traceback.format_exc())
213 214 return HTTPInternalServerError()(environ, start_response)
214 215
215 216 def __make_app(self, repo_name, repo_path, username):
216 217 """
217 218 Make an wsgi application using dulserver
218 219
219 220 :param repo_name: name of the repository
220 221 :param repo_path: full path to the repository
221 222 """
222 223
223 224 from rhodecode.lib.middleware.pygrack import make_wsgi_app
224 225 app = make_wsgi_app(
225 226 repo_root=safe_str(self.basepath),
226 227 repo_name=repo_name,
227 228 username=username,
228 229 )
229 230 app = GunzipFilter(LimitedInputFilter(app))
230 231 return app
231 232
232 233 def __get_repository(self, environ):
233 234 """
234 235 Get's repository name out of PATH_INFO header
235 236
236 237 :param environ: environ where PATH_INFO is stored
237 238 """
238 239 try:
239 240 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
240 241 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
241 242 except:
242 243 log.error(traceback.format_exc())
243 244 raise
244 245
245 246 return repo_name
246 247
247 248 def __get_user(self, username):
248 249 return User.get_by_username(username)
249 250
250 251 def __get_action(self, environ):
251 252 """
252 253 Maps git request commands into a pull or push command.
253 254
254 255 :param environ:
255 256 """
256 257 service = environ['QUERY_STRING'].split('=')
257 258
258 259 if len(service) > 1:
259 260 service_cmd = service[1]
260 261 mapping = {
261 262 'git-receive-pack': 'push',
262 263 'git-upload-pack': 'pull',
263 264 }
264 265 op = mapping[service_cmd]
265 266 self._git_stored_op = op
266 267 return op
267 268 else:
268 269 # try to fallback to stored variable as we don't know if the last
269 270 # operation is pull/push
270 271 op = getattr(self, '_git_stored_op', 'pull')
271 272 return op
272 273
273 274 def _handle_githooks(self, repo_name, action, baseui, environ):
274 275 """
275 276 Handles pull action, push is handled by post-receive hook
276 277 """
277 278 from rhodecode.lib.hooks import log_pull_action
278 279 service = environ['QUERY_STRING'].split('=')
279 280 if len(service) < 2:
280 281 return
281 282
282 283 from rhodecode.model.db import Repository
283 284 _repo = Repository.get_by_repo_name(repo_name)
284 285 _repo = _repo.scm_instance
285 286 _repo._repo.ui = baseui
286 287
287 288 _hooks = dict(baseui.configitems('hooks')) or {}
288 289 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
289 290 log_pull_action(ui=baseui, repo=_repo._repo)
290 291
291 292 def __inject_extras(self, repo_path, baseui, extras={}):
292 293 """
293 294 Injects some extra params into baseui instance
294 295
295 296 :param baseui: baseui instance
296 297 :param extras: dict with extra params to put into baseui
297 298 """
298 299
299 300 # make our hgweb quiet so it doesn't print output
300 301 baseui.setconfig('ui', 'quiet', 'true')
301 302
302 303 #inject some additional parameters that will be available in ui
303 304 #for hooks
304 305 for k, v in extras.items():
305 306 baseui.setconfig('rhodecode_extras', k, v)
@@ -1,258 +1,261 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import urllib
31 31
32 32 from mercurial.error import RepoError
33 33 from mercurial.hgweb import hgweb_mod
34 34
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
37 HTTPBadRequest, HTTPNotAcceptable
36 38
37 39 from rhodecode.lib.utils2 import safe_str
38 40 from rhodecode.lib.base import BaseVCSController
39 41 from rhodecode.lib.auth import get_container_username
40 42 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
41 43 from rhodecode.model.db import User
42 44
43 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48
48 49 def is_mercurial(environ):
49 50 """
50 51 Returns True if request's target is mercurial server - header
51 52 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
52 53 """
53 54 http_accept = environ.get('HTTP_ACCEPT')
54 55 path_info = environ['PATH_INFO']
55 56 if http_accept and http_accept.startswith('application/mercurial'):
56 57 ishg_path = True
57 58 else:
58 59 ishg_path = False
59 60
60 61 log.debug('pathinfo: %s detected as HG %s' % (
61 62 path_info, ishg_path)
62 63 )
63 64 return ishg_path
64 65
65 66
66 67 class SimpleHg(BaseVCSController):
67 68
68 69 def _handle_request(self, environ, start_response):
69 70 if not is_mercurial(environ):
70 71 return self.application(environ, start_response)
72 if not self._check_ssl(environ, start_response):
73 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
71 74
72 75 ipaddr = self._get_ip_addr(environ)
73 76 username = None
74 77 # skip passing error to error controller
75 78 environ['pylons.status_code_redirect'] = True
76 79
77 80 #======================================================================
78 81 # EXTRACT REPOSITORY NAME FROM ENV
79 82 #======================================================================
80 83 try:
81 84 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
82 85 log.debug('Extracted repo name is %s' % repo_name)
83 86 except:
84 87 return HTTPInternalServerError()(environ, start_response)
85 88
86 89 # quick check if that dir exists...
87 90 if is_valid_repo(repo_name, self.basepath) is False:
88 91 return HTTPNotFound()(environ, start_response)
89 92
90 93 #======================================================================
91 94 # GET ACTION PULL or PUSH
92 95 #======================================================================
93 96 action = self.__get_action(environ)
94 97
95 98 #======================================================================
96 99 # CHECK ANONYMOUS PERMISSION
97 100 #======================================================================
98 101 if action in ['pull', 'push']:
99 102 anonymous_user = self.__get_user('default')
100 103 username = anonymous_user.username
101 104 anonymous_perm = self._check_permission(action, anonymous_user,
102 105 repo_name)
103 106
104 107 if anonymous_perm is not True or anonymous_user.active is False:
105 108 if anonymous_perm is not True:
106 109 log.debug('Not enough credentials to access this '
107 110 'repository as anonymous user')
108 111 if anonymous_user.active is False:
109 112 log.debug('Anonymous access is disabled, running '
110 113 'authentication')
111 114 #==============================================================
112 115 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
113 116 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
114 117 #==============================================================
115 118
116 119 # Attempting to retrieve username from the container
117 120 username = get_container_username(environ, self.config)
118 121
119 122 # If not authenticated by the container, running basic auth
120 123 if not username:
121 124 self.authenticate.realm = \
122 125 safe_str(self.config['rhodecode_realm'])
123 126 result = self.authenticate(environ)
124 127 if isinstance(result, str):
125 128 AUTH_TYPE.update(environ, 'basic')
126 129 REMOTE_USER.update(environ, result)
127 130 username = result
128 131 else:
129 132 return result.wsgi_application(environ, start_response)
130 133
131 134 #==============================================================
132 135 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
133 136 #==============================================================
134 137 try:
135 138 user = self.__get_user(username)
136 139 if user is None or not user.active:
137 140 return HTTPForbidden()(environ, start_response)
138 141 username = user.username
139 142 except:
140 143 log.error(traceback.format_exc())
141 144 return HTTPInternalServerError()(environ, start_response)
142 145
143 146 #check permissions for this repository
144 147 perm = self._check_permission(action, user, repo_name)
145 148 if perm is not True:
146 149 return HTTPForbidden()(environ, start_response)
147 150
148 151 # extras are injected into mercurial UI object and later available
149 152 # in hg hooks executed by rhodecode
150 153 extras = {
151 154 'ip': ipaddr,
152 155 'username': username,
153 156 'action': action,
154 157 'repository': repo_name,
155 158 'scm': 'hg',
156 159 }
157 160
158 161 #======================================================================
159 162 # MERCURIAL REQUEST HANDLING
160 163 #======================================================================
161 164 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
162 165 log.debug('Repository path is %s' % repo_path)
163 166
164 167 baseui = make_ui('db')
165 168 self.__inject_extras(repo_path, baseui, extras)
166 169
167 170 try:
168 171 # invalidate cache on push
169 172 if action == 'push':
170 173 self._invalidate_cache(repo_name)
171 174 log.info('%s action on HG repo "%s"' % (action, repo_name))
172 175 app = self.__make_app(repo_path, baseui, extras)
173 176 return app(environ, start_response)
174 177 except RepoError, e:
175 178 if str(e).find('not found') != -1:
176 179 return HTTPNotFound()(environ, start_response)
177 180 except Exception:
178 181 log.error(traceback.format_exc())
179 182 return HTTPInternalServerError()(environ, start_response)
180 183
181 184 def __make_app(self, repo_name, baseui, extras):
182 185 """
183 186 Make an wsgi application using hgweb, and inject generated baseui
184 187 instance, additionally inject some extras into ui object
185 188 """
186 189 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
187 190
188 191 def __get_repository(self, environ):
189 192 """
190 193 Get's repository name out of PATH_INFO header
191 194
192 195 :param environ: environ where PATH_INFO is stored
193 196 """
194 197 try:
195 198 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
196 199 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
197 200 if repo_name.endswith('/'):
198 201 repo_name = repo_name.rstrip('/')
199 202 except:
200 203 log.error(traceback.format_exc())
201 204 raise
202 205
203 206 return repo_name
204 207
205 208 def __get_user(self, username):
206 209 return User.get_by_username(username)
207 210
208 211 def __get_action(self, environ):
209 212 """
210 213 Maps mercurial request commands into a clone,pull or push command.
211 214 This should always return a valid command string
212 215
213 216 :param environ:
214 217 """
215 218 mapping = {'changegroup': 'pull',
216 219 'changegroupsubset': 'pull',
217 220 'stream_out': 'pull',
218 221 'listkeys': 'pull',
219 222 'unbundle': 'push',
220 223 'pushkey': 'push', }
221 224 for qry in environ['QUERY_STRING'].split('&'):
222 225 if qry.startswith('cmd'):
223 226 cmd = qry.split('=')[-1]
224 227 if cmd in mapping:
225 228 return mapping[cmd]
226 229
227 230 return 'pull'
228 231
229 232 raise Exception('Unable to detect pull/push action !!'
230 233 'Are you using non standard command or client ?')
231 234
232 235 def __inject_extras(self, repo_path, baseui, extras={}):
233 236 """
234 237 Injects some extra params into baseui instance
235 238
236 239 also overwrites global settings with those takes from local hgrc file
237 240
238 241 :param baseui: baseui instance
239 242 :param extras: dict with extra params to put into baseui
240 243 """
241 244
242 245 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
243 246
244 247 # make our hgweb quiet so it doesn't print output
245 248 baseui.setconfig('ui', 'quiet', 'true')
246 249
247 250 #inject some additional parameters that will be available in ui
248 251 #for hooks
249 252 for k, v in extras.items():
250 253 baseui.setconfig('rhodecode_extras', k, v)
251 254
252 255 repoui = make_ui('file', hgrc, False)
253 256
254 257 if repoui:
255 258 #overwrite our ui instance with the section from hgrc file
256 259 for section in ui_sections:
257 260 for k, v in repoui.configitems(section):
258 261 baseui.setconfig(section, k, v)
@@ -1,703 +1,703 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 from os.path import abspath
36 36 from os.path import dirname as dn, join as jn
37 37
38 38 from paste.script.command import Command, BadCommand
39 39
40 40 from mercurial import ui, config
41 41
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43
44 44 from rhodecode.lib.vcs import get_backend
45 45 from rhodecode.lib.vcs.backends.base import BaseChangeset
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.utils.helpers import get_scm
48 48 from rhodecode.lib.vcs.exceptions import VCSError
49 49
50 50 from rhodecode.lib.caching_query import FromCache
51 51
52 52 from rhodecode.model import meta
53 53 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
54 54 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.repos_group import ReposGroupModel
57 57 from rhodecode.lib.utils2 import safe_str, safe_unicode
58 58 from rhodecode.lib.vcs.utils.fakemod import create_module
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
63 63
64 64
65 65 def recursive_replace(str_, replace=' '):
66 66 """
67 67 Recursive replace of given sign to just one instance
68 68
69 69 :param str_: given string
70 70 :param replace: char to find and replace multiple instances
71 71
72 72 Examples::
73 73 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
74 74 'Mighty-Mighty-Bo-sstones'
75 75 """
76 76
77 77 if str_.find(replace * 2) == -1:
78 78 return str_
79 79 else:
80 80 str_ = str_.replace(replace * 2, replace)
81 81 return recursive_replace(str_, replace)
82 82
83 83
84 84 def repo_name_slug(value):
85 85 """
86 86 Return slug of name of repository
87 87 This function is called on each creation/modification
88 88 of repository to prevent bad names in repo
89 89 """
90 90
91 91 slug = remove_formatting(value)
92 92 slug = strip_tags(slug)
93 93
94 94 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
95 95 slug = slug.replace(c, '-')
96 96 slug = recursive_replace(slug, '-')
97 97 slug = collapse(slug, '-')
98 98 return slug
99 99
100 100
101 101 def get_repo_slug(request):
102 102 _repo = request.environ['pylons.routes_dict'].get('repo_name')
103 103 if _repo:
104 104 _repo = _repo.rstrip('/')
105 105 return _repo
106 106
107 107
108 108 def get_repos_group_slug(request):
109 109 _group = request.environ['pylons.routes_dict'].get('group_name')
110 110 if _group:
111 111 _group = _group.rstrip('/')
112 112 return _group
113 113
114 114
115 115 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
116 116 """
117 117 Action logger for various actions made by users
118 118
119 119 :param user: user that made this action, can be a unique username string or
120 120 object containing user_id attribute
121 121 :param action: action to log, should be on of predefined unique actions for
122 122 easy translations
123 123 :param repo: string name of repository or object containing repo_id,
124 124 that action was made on
125 125 :param ipaddr: optional ip address from what the action was made
126 126 :param sa: optional sqlalchemy session
127 127
128 128 """
129 129
130 130 if not sa:
131 131 sa = meta.Session()
132 132
133 133 try:
134 134 if hasattr(user, 'user_id'):
135 135 user_obj = user
136 136 elif isinstance(user, basestring):
137 137 user_obj = User.get_by_username(user)
138 138 else:
139 139 raise Exception('You have to provide user object or username')
140 140
141 141 if hasattr(repo, 'repo_id'):
142 142 repo_obj = Repository.get(repo.repo_id)
143 143 repo_name = repo_obj.repo_name
144 144 elif isinstance(repo, basestring):
145 145 repo_name = repo.lstrip('/')
146 146 repo_obj = Repository.get_by_repo_name(repo_name)
147 147 else:
148 148 repo_obj = None
149 149 repo_name = ''
150 150
151 151 user_log = UserLog()
152 152 user_log.user_id = user_obj.user_id
153 153 user_log.action = safe_unicode(action)
154 154
155 155 user_log.repository = repo_obj
156 156 user_log.repository_name = repo_name
157 157
158 158 user_log.action_date = datetime.datetime.now()
159 159 user_log.user_ip = ipaddr
160 160 sa.add(user_log)
161 161
162 162 log.info(
163 163 'Adding user %s, action %s on %s' % (user_obj, action,
164 164 safe_unicode(repo))
165 165 )
166 166 if commit:
167 167 sa.commit()
168 168 except:
169 169 log.error(traceback.format_exc())
170 170 raise
171 171
172 172
173 173 def get_repos(path, recursive=False):
174 174 """
175 175 Scans given path for repos and return (name,(type,path)) tuple
176 176
177 177 :param path: path to scan for repositories
178 178 :param recursive: recursive search and return names with subdirs in front
179 179 """
180 180
181 181 # remove ending slash for better results
182 182 path = path.rstrip(os.sep)
183 183
184 184 def _get_repos(p):
185 185 if not os.access(p, os.W_OK):
186 186 return
187 187 for dirpath in os.listdir(p):
188 188 if os.path.isfile(os.path.join(p, dirpath)):
189 189 continue
190 190 cur_path = os.path.join(p, dirpath)
191 191 try:
192 192 scm_info = get_scm(cur_path)
193 193 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
194 194 except VCSError:
195 195 if not recursive:
196 196 continue
197 197 #check if this dir containts other repos for recursive scan
198 198 rec_path = os.path.join(p, dirpath)
199 199 if os.path.isdir(rec_path):
200 200 for inner_scm in _get_repos(rec_path):
201 201 yield inner_scm
202 202
203 203 return _get_repos(path)
204 204
205 205
206 206 def is_valid_repo(repo_name, base_path):
207 207 """
208 208 Returns True if given path is a valid repository False otherwise
209 209
210 210 :param repo_name:
211 211 :param base_path:
212 212
213 213 :return True: if given path is a valid repository
214 214 """
215 215 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
216 216
217 217 try:
218 218 get_scm(full_path)
219 219 return True
220 220 except VCSError:
221 221 return False
222 222
223 223
224 224 def is_valid_repos_group(repos_group_name, base_path):
225 225 """
226 226 Returns True if given path is a repos group False otherwise
227 227
228 228 :param repo_name:
229 229 :param base_path:
230 230 """
231 231 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
232 232
233 233 # check if it's not a repo
234 234 if is_valid_repo(repos_group_name, base_path):
235 235 return False
236 236
237 237 try:
238 238 # we need to check bare git repos at higher level
239 239 # since we might match branches/hooks/info/objects or possible
240 240 # other things inside bare git repo
241 241 get_scm(os.path.dirname(full_path))
242 242 return False
243 243 except VCSError:
244 244 pass
245 245
246 246 # check if it's a valid path
247 247 if os.path.isdir(full_path):
248 248 return True
249 249
250 250 return False
251 251
252 252
253 253 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
254 254 while True:
255 255 ok = raw_input(prompt)
256 256 if ok in ('y', 'ye', 'yes'):
257 257 return True
258 258 if ok in ('n', 'no', 'nop', 'nope'):
259 259 return False
260 260 retries = retries - 1
261 261 if retries < 0:
262 262 raise IOError
263 263 print complaint
264 264
265 265 #propagated from mercurial documentation
266 266 ui_sections = ['alias', 'auth',
267 267 'decode/encode', 'defaults',
268 268 'diff', 'email',
269 269 'extensions', 'format',
270 270 'merge-patterns', 'merge-tools',
271 271 'hooks', 'http_proxy',
272 272 'smtp', 'patch',
273 273 'paths', 'profiling',
274 274 'server', 'trusted',
275 275 'ui', 'web', ]
276 276
277 277
278 278 def make_ui(read_from='file', path=None, checkpaths=True):
279 279 """
280 280 A function that will read python rc files or database
281 281 and make an mercurial ui object from read options
282 282
283 283 :param path: path to mercurial config file
284 284 :param checkpaths: check the path
285 285 :param read_from: read from 'file' or 'db'
286 286 """
287 287
288 288 baseui = ui.ui()
289 289
290 290 # clean the baseui object
291 291 baseui._ocfg = config.config()
292 292 baseui._ucfg = config.config()
293 293 baseui._tcfg = config.config()
294 294
295 295 if read_from == 'file':
296 296 if not os.path.isfile(path):
297 297 log.debug('hgrc file is not present at %s skipping...' % path)
298 298 return False
299 299 log.debug('reading hgrc from %s' % path)
300 300 cfg = config.config()
301 301 cfg.read(path)
302 302 for section in ui_sections:
303 303 for k, v in cfg.items(section):
304 304 log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
305 305 baseui.setconfig(section, k, v)
306 306
307 307 elif read_from == 'db':
308 308 sa = meta.Session()
309 309 ret = sa.query(RhodeCodeUi)\
310 310 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
311 311 .all()
312 312
313 313 hg_ui = ret
314 314 for ui_ in hg_ui:
315 if ui_.ui_active:
315 if ui_.ui_active and ui_.ui_key != 'push_ssl':
316 316 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
317 317 ui_.ui_key, ui_.ui_value)
318 318 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
319 319
320 320 meta.Session.remove()
321 321 return baseui
322 322
323 323
324 324 def set_rhodecode_config(config):
325 325 """
326 326 Updates pylons config with new settings from database
327 327
328 328 :param config:
329 329 """
330 330 hgsettings = RhodeCodeSetting.get_app_settings()
331 331
332 332 for k, v in hgsettings.items():
333 333 config[k] = v
334 334
335 335
336 336 def invalidate_cache(cache_key, *args):
337 337 """
338 338 Puts cache invalidation task into db for
339 339 further global cache invalidation
340 340 """
341 341
342 342 from rhodecode.model.scm import ScmModel
343 343
344 344 if cache_key.startswith('get_repo_cached_'):
345 345 name = cache_key.split('get_repo_cached_')[-1]
346 346 ScmModel().mark_for_invalidation(name)
347 347
348 348
349 349 class EmptyChangeset(BaseChangeset):
350 350 """
351 351 An dummy empty changeset. It's possible to pass hash when creating
352 352 an EmptyChangeset
353 353 """
354 354
355 355 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
356 356 alias=None):
357 357 self._empty_cs = cs
358 358 self.revision = -1
359 359 self.message = ''
360 360 self.author = ''
361 361 self.date = ''
362 362 self.repository = repo
363 363 self.requested_revision = requested_revision
364 364 self.alias = alias
365 365
366 366 @LazyProperty
367 367 def raw_id(self):
368 368 """
369 369 Returns raw string identifying this changeset, useful for web
370 370 representation.
371 371 """
372 372
373 373 return self._empty_cs
374 374
375 375 @LazyProperty
376 376 def branch(self):
377 377 return get_backend(self.alias).DEFAULT_BRANCH_NAME
378 378
379 379 @LazyProperty
380 380 def short_id(self):
381 381 return self.raw_id[:12]
382 382
383 383 def get_file_changeset(self, path):
384 384 return self
385 385
386 386 def get_file_content(self, path):
387 387 return u''
388 388
389 389 def get_file_size(self, path):
390 390 return 0
391 391
392 392
393 393 def map_groups(path):
394 394 """
395 395 Given a full path to a repository, create all nested groups that this
396 396 repo is inside. This function creates parent-child relationships between
397 397 groups and creates default perms for all new groups.
398 398
399 399 :param paths: full path to repository
400 400 """
401 401 sa = meta.Session()
402 402 groups = path.split(Repository.url_sep())
403 403 parent = None
404 404 group = None
405 405
406 406 # last element is repo in nested groups structure
407 407 groups = groups[:-1]
408 408 rgm = ReposGroupModel(sa)
409 409 for lvl, group_name in enumerate(groups):
410 410 group_name = '/'.join(groups[:lvl] + [group_name])
411 411 group = RepoGroup.get_by_group_name(group_name)
412 412 desc = '%s group' % group_name
413 413
414 414 # skip folders that are now removed repos
415 415 if REMOVED_REPO_PAT.match(group_name):
416 416 break
417 417
418 418 if group is None:
419 419 log.debug('creating group level: %s group_name: %s' % (lvl,
420 420 group_name))
421 421 group = RepoGroup(group_name, parent)
422 422 group.group_description = desc
423 423 sa.add(group)
424 424 rgm._create_default_perms(group)
425 425 sa.flush()
426 426 parent = group
427 427 return group
428 428
429 429
430 430 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
431 431 install_git_hook=False):
432 432 """
433 433 maps all repos given in initial_repo_list, non existing repositories
434 434 are created, if remove_obsolete is True it also check for db entries
435 435 that are not in initial_repo_list and removes them.
436 436
437 437 :param initial_repo_list: list of repositories found by scanning methods
438 438 :param remove_obsolete: check for obsolete entries in database
439 439 :param install_git_hook: if this is True, also check and install githook
440 440 for a repo if missing
441 441 """
442 442 from rhodecode.model.repo import RepoModel
443 443 from rhodecode.model.scm import ScmModel
444 444 sa = meta.Session()
445 445 rm = RepoModel()
446 446 user = sa.query(User).filter(User.admin == True).first()
447 447 if user is None:
448 448 raise Exception('Missing administrative account !')
449 449 added = []
450 450
451 451 for name, repo in initial_repo_list.items():
452 452 group = map_groups(name)
453 453 db_repo = rm.get_by_repo_name(name)
454 454 # found repo that is on filesystem not in RhodeCode database
455 455 if not db_repo:
456 456 log.info('repository %s not found creating now' % name)
457 457 added.append(name)
458 458 desc = (repo.description
459 459 if repo.description != 'unknown'
460 460 else '%s repository' % name)
461 461 new_repo = rm.create_repo(
462 462 repo_name=name,
463 463 repo_type=repo.alias,
464 464 description=desc,
465 465 repos_group=getattr(group, 'group_id', None),
466 466 owner=user,
467 467 just_db=True
468 468 )
469 469 # we added that repo just now, and make sure it has githook
470 470 # installed
471 471 if new_repo.repo_type == 'git':
472 472 ScmModel().install_git_hook(new_repo.scm_instance)
473 473 elif install_git_hook:
474 474 if db_repo.repo_type == 'git':
475 475 ScmModel().install_git_hook(db_repo.scm_instance)
476 476 sa.commit()
477 477 removed = []
478 478 if remove_obsolete:
479 479 # remove from database those repositories that are not in the filesystem
480 480 for repo in sa.query(Repository).all():
481 481 if repo.repo_name not in initial_repo_list.keys():
482 482 log.debug("Removing non existing repository found in db `%s`" %
483 483 repo.repo_name)
484 484 try:
485 485 sa.delete(repo)
486 486 sa.commit()
487 487 removed.append(repo.repo_name)
488 488 except:
489 489 #don't hold further removals on error
490 490 log.error(traceback.format_exc())
491 491 sa.rollback()
492 492
493 493 # clear cache keys
494 494 log.debug("Clearing cache keys now...")
495 495 CacheInvalidation.clear_cache()
496 496 sa.commit()
497 497 return added, removed
498 498
499 499
500 500 # set cache regions for beaker so celery can utilise it
501 501 def add_cache(settings):
502 502 cache_settings = {'regions': None}
503 503 for key in settings.keys():
504 504 for prefix in ['beaker.cache.', 'cache.']:
505 505 if key.startswith(prefix):
506 506 name = key.split(prefix)[1].strip()
507 507 cache_settings[name] = settings[key].strip()
508 508 if cache_settings['regions']:
509 509 for region in cache_settings['regions'].split(','):
510 510 region = region.strip()
511 511 region_settings = {}
512 512 for key, value in cache_settings.items():
513 513 if key.startswith(region):
514 514 region_settings[key.split('.')[1]] = value
515 515 region_settings['expire'] = int(region_settings.get('expire',
516 516 60))
517 517 region_settings.setdefault('lock_dir',
518 518 cache_settings.get('lock_dir'))
519 519 region_settings.setdefault('data_dir',
520 520 cache_settings.get('data_dir'))
521 521
522 522 if 'type' not in region_settings:
523 523 region_settings['type'] = cache_settings.get('type',
524 524 'memory')
525 525 beaker.cache.cache_regions[region] = region_settings
526 526
527 527
528 528 def load_rcextensions(root_path):
529 529 import rhodecode
530 530 from rhodecode.config import conf
531 531
532 532 path = os.path.join(root_path, 'rcextensions', '__init__.py')
533 533 if os.path.isfile(path):
534 534 rcext = create_module('rc', path)
535 535 EXT = rhodecode.EXTENSIONS = rcext
536 536 log.debug('Found rcextensions now loading %s...' % rcext)
537 537
538 538 # Additional mappings that are not present in the pygments lexers
539 539 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
540 540
541 541 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
542 542
543 543 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
544 544 log.debug('settings custom INDEX_EXTENSIONS')
545 545 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
546 546
547 547 #ADDITIONAL MAPPINGS
548 548 log.debug('adding extra into INDEX_EXTENSIONS')
549 549 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
550 550
551 551
552 552 #==============================================================================
553 553 # TEST FUNCTIONS AND CREATORS
554 554 #==============================================================================
555 555 def create_test_index(repo_location, config, full_index):
556 556 """
557 557 Makes default test index
558 558
559 559 :param config: test config
560 560 :param full_index:
561 561 """
562 562
563 563 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
564 564 from rhodecode.lib.pidlock import DaemonLock, LockHeld
565 565
566 566 repo_location = repo_location
567 567
568 568 index_location = os.path.join(config['app_conf']['index_dir'])
569 569 if not os.path.exists(index_location):
570 570 os.makedirs(index_location)
571 571
572 572 try:
573 573 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
574 574 WhooshIndexingDaemon(index_location=index_location,
575 575 repo_location=repo_location)\
576 576 .run(full_index=full_index)
577 577 l.release()
578 578 except LockHeld:
579 579 pass
580 580
581 581
582 582 def create_test_env(repos_test_path, config):
583 583 """
584 584 Makes a fresh database and
585 585 install test repository into tmp dir
586 586 """
587 587 from rhodecode.lib.db_manage import DbManage
588 588 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
589 589
590 590 # PART ONE create db
591 591 dbconf = config['sqlalchemy.db1.url']
592 592 log.debug('making test db %s' % dbconf)
593 593
594 594 # create test dir if it doesn't exist
595 595 if not os.path.isdir(repos_test_path):
596 596 log.debug('Creating testdir %s' % repos_test_path)
597 597 os.makedirs(repos_test_path)
598 598
599 599 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
600 600 tests=True)
601 601 dbmanage.create_tables(override=True)
602 602 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
603 603 dbmanage.create_default_user()
604 604 dbmanage.admin_prompt()
605 605 dbmanage.create_permissions()
606 606 dbmanage.populate_default_permissions()
607 607 Session().commit()
608 608 # PART TWO make test repo
609 609 log.debug('making test vcs repositories')
610 610
611 611 idx_path = config['app_conf']['index_dir']
612 612 data_path = config['app_conf']['cache_dir']
613 613
614 614 #clean index and data
615 615 if idx_path and os.path.exists(idx_path):
616 616 log.debug('remove %s' % idx_path)
617 617 shutil.rmtree(idx_path)
618 618
619 619 if data_path and os.path.exists(data_path):
620 620 log.debug('remove %s' % data_path)
621 621 shutil.rmtree(data_path)
622 622
623 623 #CREATE DEFAULT TEST REPOS
624 624 cur_dir = dn(dn(abspath(__file__)))
625 625 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
626 626 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
627 627 tar.close()
628 628
629 629 cur_dir = dn(dn(abspath(__file__)))
630 630 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
631 631 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
632 632 tar.close()
633 633
634 634 #LOAD VCS test stuff
635 635 from rhodecode.tests.vcs import setup_package
636 636 setup_package()
637 637
638 638
639 639 #==============================================================================
640 640 # PASTER COMMANDS
641 641 #==============================================================================
642 642 class BasePasterCommand(Command):
643 643 """
644 644 Abstract Base Class for paster commands.
645 645
646 646 The celery commands are somewhat aggressive about loading
647 647 celery.conf, and since our module sets the `CELERY_LOADER`
648 648 environment variable to our loader, we have to bootstrap a bit and
649 649 make sure we've had a chance to load the pylons config off of the
650 650 command line, otherwise everything fails.
651 651 """
652 652 min_args = 1
653 653 min_args_error = "Please provide a paster config file as an argument."
654 654 takes_config_file = 1
655 655 requires_config_file = True
656 656
657 657 def notify_msg(self, msg, log=False):
658 658 """Make a notification to user, additionally if logger is passed
659 659 it logs this action using given logger
660 660
661 661 :param msg: message that will be printed to user
662 662 :param log: logging instance, to use to additionally log this message
663 663
664 664 """
665 665 if log and isinstance(log, logging):
666 666 log(msg)
667 667
668 668 def run(self, args):
669 669 """
670 670 Overrides Command.run
671 671
672 672 Checks for a config file argument and loads it.
673 673 """
674 674 if len(args) < self.min_args:
675 675 raise BadCommand(
676 676 self.min_args_error % {'min_args': self.min_args,
677 677 'actual_args': len(args)})
678 678
679 679 # Decrement because we're going to lob off the first argument.
680 680 # @@ This is hacky
681 681 self.min_args -= 1
682 682 self.bootstrap_config(args[0])
683 683 self.update_parser()
684 684 return super(BasePasterCommand, self).run(args[1:])
685 685
686 686 def update_parser(self):
687 687 """
688 688 Abstract method. Allows for the class's parser to be updated
689 689 before the superclass's `run` method is called. Necessary to
690 690 allow options/arguments to be passed through to the underlying
691 691 celery command.
692 692 """
693 693 raise NotImplementedError("Abstract Method.")
694 694
695 695 def bootstrap_config(self, conf):
696 696 """
697 697 Loads the pylons configuration.
698 698 """
699 699 from pylons import config as pylonsconfig
700 700
701 701 self.path_to_ini_file = os.path.realpath(conf)
702 702 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
703 703 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,1672 +1,1672 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) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 from collections import defaultdict
32 32
33 33 from sqlalchemy import *
34 34 from sqlalchemy.ext.hybrid import hybrid_property
35 35 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
36 36 from sqlalchemy.exc import DatabaseError
37 37 from beaker.cache import cache_region, region_invalidate
38 38 from webob.exc import HTTPNotFound
39 39
40 40 from pylons.i18n.translation import lazy_ugettext as _
41 41
42 42 from rhodecode.lib.vcs import get_backend
43 43 from rhodecode.lib.vcs.utils.helpers import get_scm
44 44 from rhodecode.lib.vcs.exceptions import VCSError
45 45 from rhodecode.lib.vcs.utils.lazy import LazyProperty
46 46
47 47 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
48 48 safe_unicode
49 49 from rhodecode.lib.compat import json
50 50 from rhodecode.lib.caching_query import FromCache
51 51
52 52 from rhodecode.model.meta import Base, Session
53 53
54 54 URL_SEP = '/'
55 55 log = logging.getLogger(__name__)
56 56
57 57 #==============================================================================
58 58 # BASE CLASSES
59 59 #==============================================================================
60 60
61 61 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
62 62
63 63
64 64 class BaseModel(object):
65 65 """
66 66 Base Model for all classess
67 67 """
68 68
69 69 @classmethod
70 70 def _get_keys(cls):
71 71 """return column names for this model """
72 72 return class_mapper(cls).c.keys()
73 73
74 74 def get_dict(self):
75 75 """
76 76 return dict with keys and values corresponding
77 77 to this model data """
78 78
79 79 d = {}
80 80 for k in self._get_keys():
81 81 d[k] = getattr(self, k)
82 82
83 83 # also use __json__() if present to get additional fields
84 84 _json_attr = getattr(self, '__json__', None)
85 85 if _json_attr:
86 86 # update with attributes from __json__
87 87 if callable(_json_attr):
88 88 _json_attr = _json_attr()
89 89 for k, val in _json_attr.iteritems():
90 90 d[k] = val
91 91 return d
92 92
93 93 def get_appstruct(self):
94 94 """return list with keys and values tupples corresponding
95 95 to this model data """
96 96
97 97 l = []
98 98 for k in self._get_keys():
99 99 l.append((k, getattr(self, k),))
100 100 return l
101 101
102 102 def populate_obj(self, populate_dict):
103 103 """populate model with data from given populate_dict"""
104 104
105 105 for k in self._get_keys():
106 106 if k in populate_dict:
107 107 setattr(self, k, populate_dict[k])
108 108
109 109 @classmethod
110 110 def query(cls):
111 111 return Session().query(cls)
112 112
113 113 @classmethod
114 114 def get(cls, id_):
115 115 if id_:
116 116 return cls.query().get(id_)
117 117
118 118 @classmethod
119 119 def get_or_404(cls, id_):
120 120 if id_:
121 121 res = cls.query().get(id_)
122 122 if not res:
123 123 raise HTTPNotFound
124 124 return res
125 125
126 126 @classmethod
127 127 def getAll(cls):
128 128 return cls.query().all()
129 129
130 130 @classmethod
131 131 def delete(cls, id_):
132 132 obj = cls.query().get(id_)
133 133 Session().delete(obj)
134 134
135 135 def __repr__(self):
136 136 if hasattr(self, '__unicode__'):
137 137 # python repr needs to return str
138 138 return safe_str(self.__unicode__())
139 139 return '<DB:%s>' % (self.__class__.__name__)
140 140
141 141
142 142 class RhodeCodeSetting(Base, BaseModel):
143 143 __tablename__ = 'rhodecode_settings'
144 144 __table_args__ = (
145 145 UniqueConstraint('app_settings_name'),
146 146 {'extend_existing': True, 'mysql_engine': 'InnoDB',
147 147 'mysql_charset': 'utf8'}
148 148 )
149 149 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
150 150 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
151 151 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
152 152
153 153 def __init__(self, k='', v=''):
154 154 self.app_settings_name = k
155 155 self.app_settings_value = v
156 156
157 157 @validates('_app_settings_value')
158 158 def validate_settings_value(self, key, val):
159 159 assert type(val) == unicode
160 160 return val
161 161
162 162 @hybrid_property
163 163 def app_settings_value(self):
164 164 v = self._app_settings_value
165 165 if self.app_settings_name == 'ldap_active':
166 166 v = str2bool(v)
167 167 return v
168 168
169 169 @app_settings_value.setter
170 170 def app_settings_value(self, val):
171 171 """
172 172 Setter that will always make sure we use unicode in app_settings_value
173 173
174 174 :param val:
175 175 """
176 176 self._app_settings_value = safe_unicode(val)
177 177
178 178 def __unicode__(self):
179 179 return u"<%s('%s:%s')>" % (
180 180 self.__class__.__name__,
181 181 self.app_settings_name, self.app_settings_value
182 182 )
183 183
184 184 @classmethod
185 185 def get_by_name(cls, ldap_key):
186 186 return cls.query()\
187 187 .filter(cls.app_settings_name == ldap_key).scalar()
188 188
189 189 @classmethod
190 190 def get_app_settings(cls, cache=False):
191 191
192 192 ret = cls.query()
193 193
194 194 if cache:
195 195 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
196 196
197 197 if not ret:
198 198 raise Exception('Could not get application settings !')
199 199 settings = {}
200 200 for each in ret:
201 201 settings['rhodecode_' + each.app_settings_name] = \
202 202 each.app_settings_value
203 203
204 204 return settings
205 205
206 206 @classmethod
207 207 def get_ldap_settings(cls, cache=False):
208 208 ret = cls.query()\
209 209 .filter(cls.app_settings_name.startswith('ldap_')).all()
210 210 fd = {}
211 211 for row in ret:
212 212 fd.update({row.app_settings_name: row.app_settings_value})
213 213
214 214 return fd
215 215
216 216
217 217 class RhodeCodeUi(Base, BaseModel):
218 218 __tablename__ = 'rhodecode_ui'
219 219 __table_args__ = (
220 220 UniqueConstraint('ui_key'),
221 221 {'extend_existing': True, 'mysql_engine': 'InnoDB',
222 222 'mysql_charset': 'utf8'}
223 223 )
224 224
225 225 HOOK_UPDATE = 'changegroup.update'
226 226 HOOK_REPO_SIZE = 'changegroup.repo_size'
227 227 HOOK_PUSH = 'changegroup.push_logger'
228 228 HOOK_PULL = 'preoutgoing.pull_logger'
229 229
230 230 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
231 231 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
232 232 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
233 233 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
234 234 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
235 235
236 236 @classmethod
237 237 def get_by_key(cls, key):
238 238 return cls.query().filter(cls.ui_key == key)
239 239
240 240 @classmethod
241 241 def get_builtin_hooks(cls):
242 242 q = cls.query()
243 243 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
244 244 cls.HOOK_REPO_SIZE,
245 245 cls.HOOK_PUSH, cls.HOOK_PULL]))
246 246 return q.all()
247 247
248 248 @classmethod
249 249 def get_custom_hooks(cls):
250 250 q = cls.query()
251 251 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
252 252 cls.HOOK_REPO_SIZE,
253 253 cls.HOOK_PUSH, cls.HOOK_PULL]))
254 254 q = q.filter(cls.ui_section == 'hooks')
255 255 return q.all()
256 256
257 257 @classmethod
258 258 def get_repos_location(cls):
259 259 return cls.get_by_key('/').one().ui_value
260 260
261 261 @classmethod
262 262 def create_or_update_hook(cls, key, val):
263 263 new_ui = cls.get_by_key(key).scalar() or cls()
264 264 new_ui.ui_section = 'hooks'
265 265 new_ui.ui_active = True
266 266 new_ui.ui_key = key
267 267 new_ui.ui_value = val
268 268
269 269 Session().add(new_ui)
270 270
271 271
272 272 class User(Base, BaseModel):
273 273 __tablename__ = 'users'
274 274 __table_args__ = (
275 275 UniqueConstraint('username'), UniqueConstraint('email'),
276 276 {'extend_existing': True, 'mysql_engine': 'InnoDB',
277 277 'mysql_charset': 'utf8'}
278 278 )
279 279 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
280 280 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
281 281 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
282 282 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
283 283 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
284 284 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
285 285 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
286 286 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
287 287 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
288 288 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
289 289 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
290 290
291 291 user_log = relationship('UserLog', cascade='all')
292 292 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
293 293
294 294 repositories = relationship('Repository')
295 295 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
296 296 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
297 297 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
298 298
299 299 group_member = relationship('UsersGroupMember', cascade='all')
300 300
301 301 notifications = relationship('UserNotification', cascade='all')
302 302 # notifications assigned to this user
303 303 user_created_notifications = relationship('Notification', cascade='all')
304 304 # comments created by this user
305 305 user_comments = relationship('ChangesetComment', cascade='all')
306 306 #extra emails for this user
307 307 user_emails = relationship('UserEmailMap', cascade='all')
308 308
309 309 @hybrid_property
310 310 def email(self):
311 311 return self._email
312 312
313 313 @email.setter
314 314 def email(self, val):
315 315 self._email = val.lower() if val else None
316 316
317 317 @property
318 318 def emails(self):
319 319 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
320 320 return [self.email] + [x.email for x in other]
321 321
322 322 @property
323 323 def full_name(self):
324 324 return '%s %s' % (self.name, self.lastname)
325 325
326 326 @property
327 327 def full_name_or_username(self):
328 328 return ('%s %s' % (self.name, self.lastname)
329 329 if (self.name and self.lastname) else self.username)
330 330
331 331 @property
332 332 def full_contact(self):
333 333 return '%s %s <%s>' % (self.name, self.lastname, self.email)
334 334
335 335 @property
336 336 def short_contact(self):
337 337 return '%s %s' % (self.name, self.lastname)
338 338
339 339 @property
340 340 def is_admin(self):
341 341 return self.admin
342 342
343 343 def __unicode__(self):
344 344 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
345 345 self.user_id, self.username)
346 346
347 347 @classmethod
348 348 def get_by_username(cls, username, case_insensitive=False, cache=False):
349 349 if case_insensitive:
350 350 q = cls.query().filter(cls.username.ilike(username))
351 351 else:
352 352 q = cls.query().filter(cls.username == username)
353 353
354 354 if cache:
355 355 q = q.options(FromCache(
356 356 "sql_cache_short",
357 357 "get_user_%s" % _hash_key(username)
358 358 )
359 359 )
360 360 return q.scalar()
361 361
362 362 @classmethod
363 363 def get_by_api_key(cls, api_key, cache=False):
364 364 q = cls.query().filter(cls.api_key == api_key)
365 365
366 366 if cache:
367 367 q = q.options(FromCache("sql_cache_short",
368 368 "get_api_key_%s" % api_key))
369 369 return q.scalar()
370 370
371 371 @classmethod
372 372 def get_by_email(cls, email, case_insensitive=False, cache=False):
373 373 if case_insensitive:
374 374 q = cls.query().filter(cls.email.ilike(email))
375 375 else:
376 376 q = cls.query().filter(cls.email == email)
377 377
378 378 if cache:
379 379 q = q.options(FromCache("sql_cache_short",
380 380 "get_email_key_%s" % email))
381 381
382 382 ret = q.scalar()
383 383 if ret is None:
384 384 q = UserEmailMap.query()
385 385 # try fetching in alternate email map
386 386 if case_insensitive:
387 387 q = q.filter(UserEmailMap.email.ilike(email))
388 388 else:
389 389 q = q.filter(UserEmailMap.email == email)
390 390 q = q.options(joinedload(UserEmailMap.user))
391 391 if cache:
392 392 q = q.options(FromCache("sql_cache_short",
393 393 "get_email_map_key_%s" % email))
394 394 ret = getattr(q.scalar(), 'user', None)
395 395
396 396 return ret
397 397
398 398 def update_lastlogin(self):
399 399 """Update user lastlogin"""
400 400 self.last_login = datetime.datetime.now()
401 401 Session().add(self)
402 402 log.debug('updated user %s lastlogin' % self.username)
403 403
404 404 def get_api_data(self):
405 405 """
406 406 Common function for generating user related data for API
407 407 """
408 408 user = self
409 409 data = dict(
410 410 user_id=user.user_id,
411 411 username=user.username,
412 412 firstname=user.name,
413 413 lastname=user.lastname,
414 414 email=user.email,
415 415 emails=user.emails,
416 416 api_key=user.api_key,
417 417 active=user.active,
418 418 admin=user.admin,
419 419 ldap_dn=user.ldap_dn,
420 420 last_login=user.last_login,
421 421 )
422 422 return data
423 423
424 424 def __json__(self):
425 425 data = dict(
426 426 full_name=self.full_name,
427 427 full_name_or_username=self.full_name_or_username,
428 428 short_contact=self.short_contact,
429 429 full_contact=self.full_contact
430 430 )
431 431 data.update(self.get_api_data())
432 432 return data
433 433
434 434
435 435 class UserEmailMap(Base, BaseModel):
436 436 __tablename__ = 'user_email_map'
437 437 __table_args__ = (
438 438 Index('uem_email_idx', 'email'),
439 439 UniqueConstraint('email'),
440 440 {'extend_existing': True, 'mysql_engine': 'InnoDB',
441 441 'mysql_charset': 'utf8'}
442 442 )
443 443 __mapper_args__ = {}
444 444
445 445 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
446 446 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
447 447 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
448 448
449 449 user = relationship('User', lazy='joined')
450 450
451 451 @validates('_email')
452 452 def validate_email(self, key, email):
453 453 # check if this email is not main one
454 454 main_email = Session().query(User).filter(User.email == email).scalar()
455 455 if main_email is not None:
456 456 raise AttributeError('email %s is present is user table' % email)
457 457 return email
458 458
459 459 @hybrid_property
460 460 def email(self):
461 461 return self._email
462 462
463 463 @email.setter
464 464 def email(self, val):
465 465 self._email = val.lower() if val else None
466 466
467 467
468 468 class UserLog(Base, BaseModel):
469 469 __tablename__ = 'user_logs'
470 470 __table_args__ = (
471 471 {'extend_existing': True, 'mysql_engine': 'InnoDB',
472 472 'mysql_charset': 'utf8'},
473 473 )
474 474 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
475 475 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
476 476 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
477 477 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
478 478 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
479 479 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
480 480 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
481 481
482 482 @property
483 483 def action_as_day(self):
484 484 return datetime.date(*self.action_date.timetuple()[:3])
485 485
486 486 user = relationship('User')
487 487 repository = relationship('Repository', cascade='')
488 488
489 489
490 490 class UsersGroup(Base, BaseModel):
491 491 __tablename__ = 'users_groups'
492 492 __table_args__ = (
493 493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 494 'mysql_charset': 'utf8'},
495 495 )
496 496
497 497 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
498 498 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
499 499 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
500 500
501 501 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
502 502 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
503 503 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
504 504
505 505 def __unicode__(self):
506 506 return u'<userGroup(%s)>' % (self.users_group_name)
507 507
508 508 @classmethod
509 509 def get_by_group_name(cls, group_name, cache=False,
510 510 case_insensitive=False):
511 511 if case_insensitive:
512 512 q = cls.query().filter(cls.users_group_name.ilike(group_name))
513 513 else:
514 514 q = cls.query().filter(cls.users_group_name == group_name)
515 515 if cache:
516 516 q = q.options(FromCache(
517 517 "sql_cache_short",
518 518 "get_user_%s" % _hash_key(group_name)
519 519 )
520 520 )
521 521 return q.scalar()
522 522
523 523 @classmethod
524 524 def get(cls, users_group_id, cache=False):
525 525 users_group = cls.query()
526 526 if cache:
527 527 users_group = users_group.options(FromCache("sql_cache_short",
528 528 "get_users_group_%s" % users_group_id))
529 529 return users_group.get(users_group_id)
530 530
531 531 def get_api_data(self):
532 532 users_group = self
533 533
534 534 data = dict(
535 535 users_group_id=users_group.users_group_id,
536 536 group_name=users_group.users_group_name,
537 537 active=users_group.users_group_active,
538 538 )
539 539
540 540 return data
541 541
542 542
543 543 class UsersGroupMember(Base, BaseModel):
544 544 __tablename__ = 'users_groups_members'
545 545 __table_args__ = (
546 546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
547 547 'mysql_charset': 'utf8'},
548 548 )
549 549
550 550 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
551 551 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
552 552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
553 553
554 554 user = relationship('User', lazy='joined')
555 555 users_group = relationship('UsersGroup')
556 556
557 557 def __init__(self, gr_id='', u_id=''):
558 558 self.users_group_id = gr_id
559 559 self.user_id = u_id
560 560
561 561
562 562 class Repository(Base, BaseModel):
563 563 __tablename__ = 'repositories'
564 564 __table_args__ = (
565 565 UniqueConstraint('repo_name'),
566 566 {'extend_existing': True, 'mysql_engine': 'InnoDB',
567 567 'mysql_charset': 'utf8'},
568 568 )
569 569
570 570 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 571 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
572 572 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
573 573 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
574 574 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
575 575 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
576 576 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
577 577 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
578 578 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
579 579 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
580 580 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
581 581
582 582 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
583 583 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
584 584
585 585 user = relationship('User')
586 586 fork = relationship('Repository', remote_side=repo_id)
587 587 group = relationship('RepoGroup')
588 588 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
589 589 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
590 590 stats = relationship('Statistics', cascade='all', uselist=False)
591 591
592 592 followers = relationship('UserFollowing',
593 593 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
594 594 cascade='all')
595 595
596 596 logs = relationship('UserLog')
597 597 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
598 598
599 599 pull_requests_org = relationship('PullRequest',
600 600 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
601 601 cascade="all, delete, delete-orphan")
602 602
603 603 pull_requests_other = relationship('PullRequest',
604 604 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
605 605 cascade="all, delete, delete-orphan")
606 606
607 607 def __unicode__(self):
608 608 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
609 609 self.repo_name)
610 610
611 611 @classmethod
612 612 def url_sep(cls):
613 613 return URL_SEP
614 614
615 615 @classmethod
616 616 def get_by_repo_name(cls, repo_name):
617 617 q = Session().query(cls).filter(cls.repo_name == repo_name)
618 618 q = q.options(joinedload(Repository.fork))\
619 619 .options(joinedload(Repository.user))\
620 620 .options(joinedload(Repository.group))
621 621 return q.scalar()
622 622
623 623 @classmethod
624 624 def get_by_full_path(cls, repo_full_path):
625 625 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
626 626 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
627 627
628 628 @classmethod
629 629 def get_repo_forks(cls, repo_id):
630 630 return cls.query().filter(Repository.fork_id == repo_id)
631 631
632 632 @classmethod
633 633 def base_path(cls):
634 634 """
635 635 Returns base path when all repos are stored
636 636
637 637 :param cls:
638 638 """
639 639 q = Session().query(RhodeCodeUi)\
640 640 .filter(RhodeCodeUi.ui_key == cls.url_sep())
641 641 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
642 642 return q.one().ui_value
643 643
644 644 @property
645 645 def forks(self):
646 646 """
647 647 Return forks of this repo
648 648 """
649 649 return Repository.get_repo_forks(self.repo_id)
650 650
651 651 @property
652 652 def parent(self):
653 653 """
654 654 Returns fork parent
655 655 """
656 656 return self.fork
657 657
658 658 @property
659 659 def just_name(self):
660 660 return self.repo_name.split(Repository.url_sep())[-1]
661 661
662 662 @property
663 663 def groups_with_parents(self):
664 664 groups = []
665 665 if self.group is None:
666 666 return groups
667 667
668 668 cur_gr = self.group
669 669 groups.insert(0, cur_gr)
670 670 while 1:
671 671 gr = getattr(cur_gr, 'parent_group', None)
672 672 cur_gr = cur_gr.parent_group
673 673 if gr is None:
674 674 break
675 675 groups.insert(0, gr)
676 676
677 677 return groups
678 678
679 679 @property
680 680 def groups_and_repo(self):
681 681 return self.groups_with_parents, self.just_name
682 682
683 683 @LazyProperty
684 684 def repo_path(self):
685 685 """
686 686 Returns base full path for that repository means where it actually
687 687 exists on a filesystem
688 688 """
689 689 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
690 690 Repository.url_sep())
691 691 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
692 692 return q.one().ui_value
693 693
694 694 @property
695 695 def repo_full_path(self):
696 696 p = [self.repo_path]
697 697 # we need to split the name by / since this is how we store the
698 698 # names in the database, but that eventually needs to be converted
699 699 # into a valid system path
700 700 p += self.repo_name.split(Repository.url_sep())
701 701 return os.path.join(*p)
702 702
703 703 def get_new_name(self, repo_name):
704 704 """
705 705 returns new full repository name based on assigned group and new new
706 706
707 707 :param group_name:
708 708 """
709 709 path_prefix = self.group.full_path_splitted if self.group else []
710 710 return Repository.url_sep().join(path_prefix + [repo_name])
711 711
712 712 @property
713 713 def _ui(self):
714 714 """
715 715 Creates an db based ui object for this repository
716 716 """
717 717 from mercurial import ui
718 718 from mercurial import config
719 719 baseui = ui.ui()
720 720
721 721 #clean the baseui object
722 722 baseui._ocfg = config.config()
723 723 baseui._ucfg = config.config()
724 724 baseui._tcfg = config.config()
725 725
726 726 ret = RhodeCodeUi.query()\
727 727 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
728 728
729 729 hg_ui = ret
730 730 for ui_ in hg_ui:
731 if ui_.ui_active:
731 if ui_.ui_active and ui_.ui_key != 'push_ssl':
732 732 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
733 733 ui_.ui_key, ui_.ui_value)
734 734 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
735 735
736 736 return baseui
737 737
738 738 @classmethod
739 739 def inject_ui(cls, repo, extras={}):
740 740 from rhodecode.lib.vcs.backends.hg import MercurialRepository
741 741 from rhodecode.lib.vcs.backends.git import GitRepository
742 742 required = (MercurialRepository, GitRepository)
743 743 if not isinstance(repo, required):
744 744 raise Exception('repo must be instance of %s' % required)
745 745
746 746 # inject ui extra param to log this action via push logger
747 747 for k, v in extras.items():
748 748 repo._repo.ui.setconfig('rhodecode_extras', k, v)
749 749
750 750 @classmethod
751 751 def is_valid(cls, repo_name):
752 752 """
753 753 returns True if given repo name is a valid filesystem repository
754 754
755 755 :param cls:
756 756 :param repo_name:
757 757 """
758 758 from rhodecode.lib.utils import is_valid_repo
759 759
760 760 return is_valid_repo(repo_name, cls.base_path())
761 761
762 762 def get_api_data(self):
763 763 """
764 764 Common function for generating repo api data
765 765
766 766 """
767 767 repo = self
768 768 data = dict(
769 769 repo_id=repo.repo_id,
770 770 repo_name=repo.repo_name,
771 771 repo_type=repo.repo_type,
772 772 clone_uri=repo.clone_uri,
773 773 private=repo.private,
774 774 created_on=repo.created_on,
775 775 description=repo.description,
776 776 landing_rev=repo.landing_rev,
777 777 owner=repo.user.username,
778 778 fork_of=repo.fork.repo_name if repo.fork else None
779 779 )
780 780
781 781 return data
782 782
783 783 #==========================================================================
784 784 # SCM PROPERTIES
785 785 #==========================================================================
786 786
787 787 def get_changeset(self, rev=None):
788 788 return get_changeset_safe(self.scm_instance, rev)
789 789
790 790 def get_landing_changeset(self):
791 791 """
792 792 Returns landing changeset, or if that doesn't exist returns the tip
793 793 """
794 794 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
795 795 return cs
796 796
797 797 @property
798 798 def tip(self):
799 799 return self.get_changeset('tip')
800 800
801 801 @property
802 802 def author(self):
803 803 return self.tip.author
804 804
805 805 @property
806 806 def last_change(self):
807 807 return self.scm_instance.last_change
808 808
809 809 def get_comments(self, revisions=None):
810 810 """
811 811 Returns comments for this repository grouped by revisions
812 812
813 813 :param revisions: filter query by revisions only
814 814 """
815 815 cmts = ChangesetComment.query()\
816 816 .filter(ChangesetComment.repo == self)
817 817 if revisions:
818 818 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
819 819 grouped = defaultdict(list)
820 820 for cmt in cmts.all():
821 821 grouped[cmt.revision].append(cmt)
822 822 return grouped
823 823
824 824 def statuses(self, revisions=None):
825 825 """
826 826 Returns statuses for this repository
827 827
828 828 :param revisions: list of revisions to get statuses for
829 829 :type revisions: list
830 830 """
831 831
832 832 statuses = ChangesetStatus.query()\
833 833 .filter(ChangesetStatus.repo == self)\
834 834 .filter(ChangesetStatus.version == 0)
835 835 if revisions:
836 836 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
837 837 grouped = {}
838 838
839 839 #maybe we have open new pullrequest without a status ?
840 840 stat = ChangesetStatus.STATUS_UNDER_REVIEW
841 841 status_lbl = ChangesetStatus.get_status_lbl(stat)
842 842 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
843 843 for rev in pr.revisions:
844 844 pr_id = pr.pull_request_id
845 845 pr_repo = pr.other_repo.repo_name
846 846 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
847 847
848 848 for stat in statuses.all():
849 849 pr_id = pr_repo = None
850 850 if stat.pull_request:
851 851 pr_id = stat.pull_request.pull_request_id
852 852 pr_repo = stat.pull_request.other_repo.repo_name
853 853 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
854 854 pr_id, pr_repo]
855 855 return grouped
856 856
857 857 #==========================================================================
858 858 # SCM CACHE INSTANCE
859 859 #==========================================================================
860 860
861 861 @property
862 862 def invalidate(self):
863 863 return CacheInvalidation.invalidate(self.repo_name)
864 864
865 865 def set_invalidate(self):
866 866 """
867 867 set a cache for invalidation for this instance
868 868 """
869 869 CacheInvalidation.set_invalidate(self.repo_name)
870 870
871 871 @LazyProperty
872 872 def scm_instance(self):
873 873 return self.__get_instance()
874 874
875 875 def scm_instance_cached(self, cache_map=None):
876 876 @cache_region('long_term')
877 877 def _c(repo_name):
878 878 return self.__get_instance()
879 879 rn = self.repo_name
880 880 log.debug('Getting cached instance of repo')
881 881
882 882 if cache_map:
883 883 # get using prefilled cache_map
884 884 invalidate_repo = cache_map[self.repo_name]
885 885 if invalidate_repo:
886 886 invalidate_repo = (None if invalidate_repo.cache_active
887 887 else invalidate_repo)
888 888 else:
889 889 # get from invalidate
890 890 invalidate_repo = self.invalidate
891 891
892 892 if invalidate_repo is not None:
893 893 region_invalidate(_c, None, rn)
894 894 # update our cache
895 895 CacheInvalidation.set_valid(invalidate_repo.cache_key)
896 896 return _c(rn)
897 897
898 898 def __get_instance(self):
899 899 repo_full_path = self.repo_full_path
900 900 try:
901 901 alias = get_scm(repo_full_path)[0]
902 902 log.debug('Creating instance of %s repository' % alias)
903 903 backend = get_backend(alias)
904 904 except VCSError:
905 905 log.error(traceback.format_exc())
906 906 log.error('Perhaps this repository is in db and not in '
907 907 'filesystem run rescan repositories with '
908 908 '"destroy old data " option from admin panel')
909 909 return
910 910
911 911 if alias == 'hg':
912 912
913 913 repo = backend(safe_str(repo_full_path), create=False,
914 914 baseui=self._ui)
915 915 # skip hidden web repository
916 916 if repo._get_hidden():
917 917 return
918 918 else:
919 919 repo = backend(repo_full_path, create=False)
920 920
921 921 return repo
922 922
923 923
924 924 class RepoGroup(Base, BaseModel):
925 925 __tablename__ = 'groups'
926 926 __table_args__ = (
927 927 UniqueConstraint('group_name', 'group_parent_id'),
928 928 CheckConstraint('group_id != group_parent_id'),
929 929 {'extend_existing': True, 'mysql_engine': 'InnoDB',
930 930 'mysql_charset': 'utf8'},
931 931 )
932 932 __mapper_args__ = {'order_by': 'group_name'}
933 933
934 934 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
935 935 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
936 936 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
937 937 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
938 938
939 939 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
940 940 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
941 941
942 942 parent_group = relationship('RepoGroup', remote_side=group_id)
943 943
944 944 def __init__(self, group_name='', parent_group=None):
945 945 self.group_name = group_name
946 946 self.parent_group = parent_group
947 947
948 948 def __unicode__(self):
949 949 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
950 950 self.group_name)
951 951
952 952 @classmethod
953 953 def groups_choices(cls):
954 954 from webhelpers.html import literal as _literal
955 955 repo_groups = [('', '')]
956 956 sep = ' &raquo; '
957 957 _name = lambda k: _literal(sep.join(k))
958 958
959 959 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
960 960 for x in cls.query().all()])
961 961
962 962 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
963 963 return repo_groups
964 964
965 965 @classmethod
966 966 def url_sep(cls):
967 967 return URL_SEP
968 968
969 969 @classmethod
970 970 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
971 971 if case_insensitive:
972 972 gr = cls.query()\
973 973 .filter(cls.group_name.ilike(group_name))
974 974 else:
975 975 gr = cls.query()\
976 976 .filter(cls.group_name == group_name)
977 977 if cache:
978 978 gr = gr.options(FromCache(
979 979 "sql_cache_short",
980 980 "get_group_%s" % _hash_key(group_name)
981 981 )
982 982 )
983 983 return gr.scalar()
984 984
985 985 @property
986 986 def parents(self):
987 987 parents_recursion_limit = 5
988 988 groups = []
989 989 if self.parent_group is None:
990 990 return groups
991 991 cur_gr = self.parent_group
992 992 groups.insert(0, cur_gr)
993 993 cnt = 0
994 994 while 1:
995 995 cnt += 1
996 996 gr = getattr(cur_gr, 'parent_group', None)
997 997 cur_gr = cur_gr.parent_group
998 998 if gr is None:
999 999 break
1000 1000 if cnt == parents_recursion_limit:
1001 1001 # this will prevent accidental infinit loops
1002 1002 log.error('group nested more than %s' %
1003 1003 parents_recursion_limit)
1004 1004 break
1005 1005
1006 1006 groups.insert(0, gr)
1007 1007 return groups
1008 1008
1009 1009 @property
1010 1010 def children(self):
1011 1011 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1012 1012
1013 1013 @property
1014 1014 def name(self):
1015 1015 return self.group_name.split(RepoGroup.url_sep())[-1]
1016 1016
1017 1017 @property
1018 1018 def full_path(self):
1019 1019 return self.group_name
1020 1020
1021 1021 @property
1022 1022 def full_path_splitted(self):
1023 1023 return self.group_name.split(RepoGroup.url_sep())
1024 1024
1025 1025 @property
1026 1026 def repositories(self):
1027 1027 return Repository.query()\
1028 1028 .filter(Repository.group == self)\
1029 1029 .order_by(Repository.repo_name)
1030 1030
1031 1031 @property
1032 1032 def repositories_recursive_count(self):
1033 1033 cnt = self.repositories.count()
1034 1034
1035 1035 def children_count(group):
1036 1036 cnt = 0
1037 1037 for child in group.children:
1038 1038 cnt += child.repositories.count()
1039 1039 cnt += children_count(child)
1040 1040 return cnt
1041 1041
1042 1042 return cnt + children_count(self)
1043 1043
1044 1044 def get_new_name(self, group_name):
1045 1045 """
1046 1046 returns new full group name based on parent and new name
1047 1047
1048 1048 :param group_name:
1049 1049 """
1050 1050 path_prefix = (self.parent_group.full_path_splitted if
1051 1051 self.parent_group else [])
1052 1052 return RepoGroup.url_sep().join(path_prefix + [group_name])
1053 1053
1054 1054
1055 1055 class Permission(Base, BaseModel):
1056 1056 __tablename__ = 'permissions'
1057 1057 __table_args__ = (
1058 1058 Index('p_perm_name_idx', 'permission_name'),
1059 1059 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1060 1060 'mysql_charset': 'utf8'},
1061 1061 )
1062 1062 PERMS = [
1063 1063 ('repository.none', _('Repository no access')),
1064 1064 ('repository.read', _('Repository read access')),
1065 1065 ('repository.write', _('Repository write access')),
1066 1066 ('repository.admin', _('Repository admin access')),
1067 1067
1068 1068 ('group.none', _('Repositories Group no access')),
1069 1069 ('group.read', _('Repositories Group read access')),
1070 1070 ('group.write', _('Repositories Group write access')),
1071 1071 ('group.admin', _('Repositories Group admin access')),
1072 1072
1073 1073 ('hg.admin', _('RhodeCode Administrator')),
1074 1074 ('hg.create.none', _('Repository creation disabled')),
1075 1075 ('hg.create.repository', _('Repository creation enabled')),
1076 1076 ('hg.register.none', _('Register disabled')),
1077 1077 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1078 1078 'with manual activation')),
1079 1079
1080 1080 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1081 1081 'with auto activation')),
1082 1082 ]
1083 1083
1084 1084 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1085 1085 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1086 1086 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1087 1087
1088 1088 def __unicode__(self):
1089 1089 return u"<%s('%s:%s')>" % (
1090 1090 self.__class__.__name__, self.permission_id, self.permission_name
1091 1091 )
1092 1092
1093 1093 @classmethod
1094 1094 def get_by_key(cls, key):
1095 1095 return cls.query().filter(cls.permission_name == key).scalar()
1096 1096
1097 1097 @classmethod
1098 1098 def get_default_perms(cls, default_user_id):
1099 1099 q = Session().query(UserRepoToPerm, Repository, cls)\
1100 1100 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1101 1101 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1102 1102 .filter(UserRepoToPerm.user_id == default_user_id)
1103 1103
1104 1104 return q.all()
1105 1105
1106 1106 @classmethod
1107 1107 def get_default_group_perms(cls, default_user_id):
1108 1108 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1109 1109 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1110 1110 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1111 1111 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1112 1112
1113 1113 return q.all()
1114 1114
1115 1115
1116 1116 class UserRepoToPerm(Base, BaseModel):
1117 1117 __tablename__ = 'repo_to_perm'
1118 1118 __table_args__ = (
1119 1119 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1120 1120 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1121 1121 'mysql_charset': 'utf8'}
1122 1122 )
1123 1123 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1124 1124 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1125 1125 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1126 1126 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1127 1127
1128 1128 user = relationship('User')
1129 1129 repository = relationship('Repository')
1130 1130 permission = relationship('Permission')
1131 1131
1132 1132 @classmethod
1133 1133 def create(cls, user, repository, permission):
1134 1134 n = cls()
1135 1135 n.user = user
1136 1136 n.repository = repository
1137 1137 n.permission = permission
1138 1138 Session().add(n)
1139 1139 return n
1140 1140
1141 1141 def __unicode__(self):
1142 1142 return u'<user:%s => %s >' % (self.user, self.repository)
1143 1143
1144 1144
1145 1145 class UserToPerm(Base, BaseModel):
1146 1146 __tablename__ = 'user_to_perm'
1147 1147 __table_args__ = (
1148 1148 UniqueConstraint('user_id', 'permission_id'),
1149 1149 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1150 1150 'mysql_charset': 'utf8'}
1151 1151 )
1152 1152 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1153 1153 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1154 1154 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1155 1155
1156 1156 user = relationship('User')
1157 1157 permission = relationship('Permission', lazy='joined')
1158 1158
1159 1159
1160 1160 class UsersGroupRepoToPerm(Base, BaseModel):
1161 1161 __tablename__ = 'users_group_repo_to_perm'
1162 1162 __table_args__ = (
1163 1163 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1164 1164 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1165 1165 'mysql_charset': 'utf8'}
1166 1166 )
1167 1167 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1168 1168 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1169 1169 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1170 1170 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1171 1171
1172 1172 users_group = relationship('UsersGroup')
1173 1173 permission = relationship('Permission')
1174 1174 repository = relationship('Repository')
1175 1175
1176 1176 @classmethod
1177 1177 def create(cls, users_group, repository, permission):
1178 1178 n = cls()
1179 1179 n.users_group = users_group
1180 1180 n.repository = repository
1181 1181 n.permission = permission
1182 1182 Session().add(n)
1183 1183 return n
1184 1184
1185 1185 def __unicode__(self):
1186 1186 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1187 1187
1188 1188
1189 1189 class UsersGroupToPerm(Base, BaseModel):
1190 1190 __tablename__ = 'users_group_to_perm'
1191 1191 __table_args__ = (
1192 1192 UniqueConstraint('users_group_id', 'permission_id',),
1193 1193 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1194 1194 'mysql_charset': 'utf8'}
1195 1195 )
1196 1196 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1197 1197 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1198 1198 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1199 1199
1200 1200 users_group = relationship('UsersGroup')
1201 1201 permission = relationship('Permission')
1202 1202
1203 1203
1204 1204 class UserRepoGroupToPerm(Base, BaseModel):
1205 1205 __tablename__ = 'user_repo_group_to_perm'
1206 1206 __table_args__ = (
1207 1207 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1208 1208 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1209 1209 'mysql_charset': 'utf8'}
1210 1210 )
1211 1211
1212 1212 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1213 1213 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1214 1214 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1215 1215 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1216 1216
1217 1217 user = relationship('User')
1218 1218 group = relationship('RepoGroup')
1219 1219 permission = relationship('Permission')
1220 1220
1221 1221
1222 1222 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1223 1223 __tablename__ = 'users_group_repo_group_to_perm'
1224 1224 __table_args__ = (
1225 1225 UniqueConstraint('users_group_id', 'group_id'),
1226 1226 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1227 1227 'mysql_charset': 'utf8'}
1228 1228 )
1229 1229
1230 1230 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1231 1231 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1232 1232 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1233 1233 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1234 1234
1235 1235 users_group = relationship('UsersGroup')
1236 1236 permission = relationship('Permission')
1237 1237 group = relationship('RepoGroup')
1238 1238
1239 1239
1240 1240 class Statistics(Base, BaseModel):
1241 1241 __tablename__ = 'statistics'
1242 1242 __table_args__ = (
1243 1243 UniqueConstraint('repository_id'),
1244 1244 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1245 1245 'mysql_charset': 'utf8'}
1246 1246 )
1247 1247 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1248 1248 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1249 1249 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1250 1250 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1251 1251 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1252 1252 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1253 1253
1254 1254 repository = relationship('Repository', single_parent=True)
1255 1255
1256 1256
1257 1257 class UserFollowing(Base, BaseModel):
1258 1258 __tablename__ = 'user_followings'
1259 1259 __table_args__ = (
1260 1260 UniqueConstraint('user_id', 'follows_repository_id'),
1261 1261 UniqueConstraint('user_id', 'follows_user_id'),
1262 1262 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1263 1263 'mysql_charset': 'utf8'}
1264 1264 )
1265 1265
1266 1266 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1267 1267 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1268 1268 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1269 1269 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1270 1270 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1271 1271
1272 1272 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1273 1273
1274 1274 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1275 1275 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1276 1276
1277 1277 @classmethod
1278 1278 def get_repo_followers(cls, repo_id):
1279 1279 return cls.query().filter(cls.follows_repo_id == repo_id)
1280 1280
1281 1281
1282 1282 class CacheInvalidation(Base, BaseModel):
1283 1283 __tablename__ = 'cache_invalidation'
1284 1284 __table_args__ = (
1285 1285 UniqueConstraint('cache_key'),
1286 1286 Index('key_idx', 'cache_key'),
1287 1287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1288 1288 'mysql_charset': 'utf8'},
1289 1289 )
1290 1290 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1291 1291 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1292 1292 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1293 1293 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1294 1294
1295 1295 def __init__(self, cache_key, cache_args=''):
1296 1296 self.cache_key = cache_key
1297 1297 self.cache_args = cache_args
1298 1298 self.cache_active = False
1299 1299
1300 1300 def __unicode__(self):
1301 1301 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1302 1302 self.cache_id, self.cache_key)
1303 1303
1304 1304 @classmethod
1305 1305 def clear_cache(cls):
1306 1306 cls.query().delete()
1307 1307
1308 1308 @classmethod
1309 1309 def _get_key(cls, key):
1310 1310 """
1311 1311 Wrapper for generating a key, together with a prefix
1312 1312
1313 1313 :param key:
1314 1314 """
1315 1315 import rhodecode
1316 1316 prefix = ''
1317 1317 iid = rhodecode.CONFIG.get('instance_id')
1318 1318 if iid:
1319 1319 prefix = iid
1320 1320 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1321 1321
1322 1322 @classmethod
1323 1323 def get_by_key(cls, key):
1324 1324 return cls.query().filter(cls.cache_key == key).scalar()
1325 1325
1326 1326 @classmethod
1327 1327 def _get_or_create_key(cls, key, prefix, org_key):
1328 1328 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1329 1329 if not inv_obj:
1330 1330 try:
1331 1331 inv_obj = CacheInvalidation(key, org_key)
1332 1332 Session().add(inv_obj)
1333 1333 Session().commit()
1334 1334 except Exception:
1335 1335 log.error(traceback.format_exc())
1336 1336 Session().rollback()
1337 1337 return inv_obj
1338 1338
1339 1339 @classmethod
1340 1340 def invalidate(cls, key):
1341 1341 """
1342 1342 Returns Invalidation object if this given key should be invalidated
1343 1343 None otherwise. `cache_active = False` means that this cache
1344 1344 state is not valid and needs to be invalidated
1345 1345
1346 1346 :param key:
1347 1347 """
1348 1348
1349 1349 key, _prefix, _org_key = cls._get_key(key)
1350 1350 inv = cls._get_or_create_key(key, _prefix, _org_key)
1351 1351
1352 1352 if inv and inv.cache_active is False:
1353 1353 return inv
1354 1354
1355 1355 @classmethod
1356 1356 def set_invalidate(cls, key):
1357 1357 """
1358 1358 Mark this Cache key for invalidation
1359 1359
1360 1360 :param key:
1361 1361 """
1362 1362
1363 1363 key, _prefix, _org_key = cls._get_key(key)
1364 1364 inv_objs = Session().query(cls).filter(cls.cache_args == _org_key).all()
1365 1365 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1366 1366 _org_key))
1367 1367 try:
1368 1368 for inv_obj in inv_objs:
1369 1369 if inv_obj:
1370 1370 inv_obj.cache_active = False
1371 1371
1372 1372 Session().add(inv_obj)
1373 1373 Session().commit()
1374 1374 except Exception:
1375 1375 log.error(traceback.format_exc())
1376 1376 Session().rollback()
1377 1377
1378 1378 @classmethod
1379 1379 def set_valid(cls, key):
1380 1380 """
1381 1381 Mark this cache key as active and currently cached
1382 1382
1383 1383 :param key:
1384 1384 """
1385 1385 inv_obj = cls.get_by_key(key)
1386 1386 inv_obj.cache_active = True
1387 1387 Session().add(inv_obj)
1388 1388 Session().commit()
1389 1389
1390 1390 @classmethod
1391 1391 def get_cache_map(cls):
1392 1392
1393 1393 class cachemapdict(dict):
1394 1394
1395 1395 def __init__(self, *args, **kwargs):
1396 1396 fixkey = kwargs.get('fixkey')
1397 1397 if fixkey:
1398 1398 del kwargs['fixkey']
1399 1399 self.fixkey = fixkey
1400 1400 super(cachemapdict, self).__init__(*args, **kwargs)
1401 1401
1402 1402 def __getattr__(self, name):
1403 1403 key = name
1404 1404 if self.fixkey:
1405 1405 key, _prefix, _org_key = cls._get_key(key)
1406 1406 if key in self.__dict__:
1407 1407 return self.__dict__[key]
1408 1408 else:
1409 1409 return self[key]
1410 1410
1411 1411 def __getitem__(self, key):
1412 1412 if self.fixkey:
1413 1413 key, _prefix, _org_key = cls._get_key(key)
1414 1414 try:
1415 1415 return super(cachemapdict, self).__getitem__(key)
1416 1416 except KeyError:
1417 1417 return
1418 1418
1419 1419 cache_map = cachemapdict(fixkey=True)
1420 1420 for obj in cls.query().all():
1421 1421 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1422 1422 return cache_map
1423 1423
1424 1424
1425 1425 class ChangesetComment(Base, BaseModel):
1426 1426 __tablename__ = 'changeset_comments'
1427 1427 __table_args__ = (
1428 1428 Index('cc_revision_idx', 'revision'),
1429 1429 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1430 1430 'mysql_charset': 'utf8'},
1431 1431 )
1432 1432 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1433 1433 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1434 1434 revision = Column('revision', String(40), nullable=True)
1435 1435 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1436 1436 line_no = Column('line_no', Unicode(10), nullable=True)
1437 1437 f_path = Column('f_path', Unicode(1000), nullable=True)
1438 1438 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1439 1439 text = Column('text', Unicode(25000), nullable=False)
1440 1440 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1441 1441 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1442 1442
1443 1443 author = relationship('User', lazy='joined')
1444 1444 repo = relationship('Repository')
1445 1445 status_change = relationship('ChangesetStatus', uselist=False)
1446 1446 pull_request = relationship('PullRequest', lazy='joined')
1447 1447
1448 1448 @classmethod
1449 1449 def get_users(cls, revision=None, pull_request_id=None):
1450 1450 """
1451 1451 Returns user associated with this ChangesetComment. ie those
1452 1452 who actually commented
1453 1453
1454 1454 :param cls:
1455 1455 :param revision:
1456 1456 """
1457 1457 q = Session().query(User)\
1458 1458 .join(ChangesetComment.author)
1459 1459 if revision:
1460 1460 q = q.filter(cls.revision == revision)
1461 1461 elif pull_request_id:
1462 1462 q = q.filter(cls.pull_request_id == pull_request_id)
1463 1463 return q.all()
1464 1464
1465 1465
1466 1466 class ChangesetStatus(Base, BaseModel):
1467 1467 __tablename__ = 'changeset_statuses'
1468 1468 __table_args__ = (
1469 1469 Index('cs_revision_idx', 'revision'),
1470 1470 Index('cs_version_idx', 'version'),
1471 1471 UniqueConstraint('repo_id', 'revision', 'version'),
1472 1472 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1473 1473 'mysql_charset': 'utf8'}
1474 1474 )
1475 1475 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1476 1476 STATUS_APPROVED = 'approved'
1477 1477 STATUS_REJECTED = 'rejected'
1478 1478 STATUS_UNDER_REVIEW = 'under_review'
1479 1479
1480 1480 STATUSES = [
1481 1481 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1482 1482 (STATUS_APPROVED, _("Approved")),
1483 1483 (STATUS_REJECTED, _("Rejected")),
1484 1484 (STATUS_UNDER_REVIEW, _("Under Review")),
1485 1485 ]
1486 1486
1487 1487 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1488 1488 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1489 1489 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1490 1490 revision = Column('revision', String(40), nullable=False)
1491 1491 status = Column('status', String(128), nullable=False, default=DEFAULT)
1492 1492 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1493 1493 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1494 1494 version = Column('version', Integer(), nullable=False, default=0)
1495 1495 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1496 1496
1497 1497 author = relationship('User', lazy='joined')
1498 1498 repo = relationship('Repository')
1499 1499 comment = relationship('ChangesetComment', lazy='joined')
1500 1500 pull_request = relationship('PullRequest', lazy='joined')
1501 1501
1502 1502 def __unicode__(self):
1503 1503 return u"<%s('%s:%s')>" % (
1504 1504 self.__class__.__name__,
1505 1505 self.status, self.author
1506 1506 )
1507 1507
1508 1508 @classmethod
1509 1509 def get_status_lbl(cls, value):
1510 1510 return dict(cls.STATUSES).get(value)
1511 1511
1512 1512 @property
1513 1513 def status_lbl(self):
1514 1514 return ChangesetStatus.get_status_lbl(self.status)
1515 1515
1516 1516
1517 1517 class PullRequest(Base, BaseModel):
1518 1518 __tablename__ = 'pull_requests'
1519 1519 __table_args__ = (
1520 1520 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1521 1521 'mysql_charset': 'utf8'},
1522 1522 )
1523 1523
1524 1524 STATUS_NEW = u'new'
1525 1525 STATUS_OPEN = u'open'
1526 1526 STATUS_CLOSED = u'closed'
1527 1527
1528 1528 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1529 1529 title = Column('title', Unicode(256), nullable=True)
1530 1530 description = Column('description', UnicodeText(10240), nullable=True)
1531 1531 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1532 1532 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1533 1533 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1534 1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1535 1535 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1536 1536 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1537 1537 org_ref = Column('org_ref', Unicode(256), nullable=False)
1538 1538 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1539 1539 other_ref = Column('other_ref', Unicode(256), nullable=False)
1540 1540
1541 1541 @hybrid_property
1542 1542 def revisions(self):
1543 1543 return self._revisions.split(':')
1544 1544
1545 1545 @revisions.setter
1546 1546 def revisions(self, val):
1547 1547 self._revisions = ':'.join(val)
1548 1548
1549 1549 author = relationship('User', lazy='joined')
1550 1550 reviewers = relationship('PullRequestReviewers',
1551 1551 cascade="all, delete, delete-orphan")
1552 1552 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1553 1553 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1554 1554 statuses = relationship('ChangesetStatus')
1555 1555 comments = relationship('ChangesetComment',
1556 1556 cascade="all, delete, delete-orphan")
1557 1557
1558 1558 def is_closed(self):
1559 1559 return self.status == self.STATUS_CLOSED
1560 1560
1561 1561 def __json__(self):
1562 1562 return dict(
1563 1563 revisions=self.revisions
1564 1564 )
1565 1565
1566 1566
1567 1567 class PullRequestReviewers(Base, BaseModel):
1568 1568 __tablename__ = 'pull_request_reviewers'
1569 1569 __table_args__ = (
1570 1570 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1571 1571 'mysql_charset': 'utf8'},
1572 1572 )
1573 1573
1574 1574 def __init__(self, user=None, pull_request=None):
1575 1575 self.user = user
1576 1576 self.pull_request = pull_request
1577 1577
1578 1578 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1579 1579 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1580 1580 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1581 1581
1582 1582 user = relationship('User')
1583 1583 pull_request = relationship('PullRequest')
1584 1584
1585 1585
1586 1586 class Notification(Base, BaseModel):
1587 1587 __tablename__ = 'notifications'
1588 1588 __table_args__ = (
1589 1589 Index('notification_type_idx', 'type'),
1590 1590 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1591 1591 'mysql_charset': 'utf8'},
1592 1592 )
1593 1593
1594 1594 TYPE_CHANGESET_COMMENT = u'cs_comment'
1595 1595 TYPE_MESSAGE = u'message'
1596 1596 TYPE_MENTION = u'mention'
1597 1597 TYPE_REGISTRATION = u'registration'
1598 1598 TYPE_PULL_REQUEST = u'pull_request'
1599 1599 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1600 1600
1601 1601 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1602 1602 subject = Column('subject', Unicode(512), nullable=True)
1603 1603 body = Column('body', UnicodeText(50000), nullable=True)
1604 1604 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1605 1605 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1606 1606 type_ = Column('type', Unicode(256))
1607 1607
1608 1608 created_by_user = relationship('User')
1609 1609 notifications_to_users = relationship('UserNotification', lazy='joined',
1610 1610 cascade="all, delete, delete-orphan")
1611 1611
1612 1612 @property
1613 1613 def recipients(self):
1614 1614 return [x.user for x in UserNotification.query()\
1615 1615 .filter(UserNotification.notification == self)\
1616 1616 .order_by(UserNotification.user_id.asc()).all()]
1617 1617
1618 1618 @classmethod
1619 1619 def create(cls, created_by, subject, body, recipients, type_=None):
1620 1620 if type_ is None:
1621 1621 type_ = Notification.TYPE_MESSAGE
1622 1622
1623 1623 notification = cls()
1624 1624 notification.created_by_user = created_by
1625 1625 notification.subject = subject
1626 1626 notification.body = body
1627 1627 notification.type_ = type_
1628 1628 notification.created_on = datetime.datetime.now()
1629 1629
1630 1630 for u in recipients:
1631 1631 assoc = UserNotification()
1632 1632 assoc.notification = notification
1633 1633 u.notifications.append(assoc)
1634 1634 Session().add(notification)
1635 1635 return notification
1636 1636
1637 1637 @property
1638 1638 def description(self):
1639 1639 from rhodecode.model.notification import NotificationModel
1640 1640 return NotificationModel().make_description(self)
1641 1641
1642 1642
1643 1643 class UserNotification(Base, BaseModel):
1644 1644 __tablename__ = 'user_to_notification'
1645 1645 __table_args__ = (
1646 1646 UniqueConstraint('user_id', 'notification_id'),
1647 1647 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1648 1648 'mysql_charset': 'utf8'}
1649 1649 )
1650 1650 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1651 1651 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1652 1652 read = Column('read', Boolean, default=False)
1653 1653 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1654 1654
1655 1655 user = relationship('User', lazy="joined")
1656 1656 notification = relationship('Notification', lazy="joined",
1657 1657 order_by=lambda: Notification.created_on.desc(),)
1658 1658
1659 1659 def mark_as_read(self):
1660 1660 self.read = True
1661 1661 Session().add(self)
1662 1662
1663 1663
1664 1664 class DbMigrateVersion(Base, BaseModel):
1665 1665 __tablename__ = 'db_migrate_version'
1666 1666 __table_args__ = (
1667 1667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1668 1668 'mysql_charset': 'utf8'},
1669 1669 )
1670 1670 repository_id = Column('repository_id', String(250), primary_key=True)
1671 1671 repository_path = Column('repository_path', Text)
1672 1672 version = Column('version', Integer)
@@ -1,246 +1,246 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Settings administration')} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_('Admin'),h.url('admin_home'))} &raquo; ${_('Settings')}
10 10 </%def>
11 11
12 12 <%def name="page_nav()">
13 13 ${self.menu('admin')}
14 14 </%def>
15 15
16 16 <%def name="main()">
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22 <!-- end box / title -->
23 23
24 24 <h3>${_('Remap and rescan repositories')}</h3>
25 25 ${h.form(url('admin_setting', setting_id='mapping'),method='put')}
26 26 <div class="form">
27 27 <!-- fields -->
28 28
29 29 <div class="fields">
30 30 <div class="field">
31 31 <div class="label label-checkbox">
32 32 <label for="destroy">${_('rescan option')}:</label>
33 33 </div>
34 34 <div class="checkboxes">
35 35 <div class="checkbox">
36 36 ${h.checkbox('destroy',True)}
37 37 <label for="destroy">
38 38 <span class="tooltip" title="${h.tooltip(_('In case a repository was deleted from filesystem and there are leftovers in the database check this option to scan obsolete data in database and remove it.'))}">
39 39 ${_('destroy old data')}</span> </label>
40 40 </div>
41 41 </div>
42 42 </div>
43 43
44 44 <div class="buttons">
45 45 ${h.submit('rescan',_('Rescan repositories'),class_="ui-btn large")}
46 46 </div>
47 47 </div>
48 48 </div>
49 49 ${h.end_form()}
50 50
51 51 <h3>${_('Whoosh indexing')}</h3>
52 52 ${h.form(url('admin_setting', setting_id='whoosh'),method='put')}
53 53 <div class="form">
54 54 <!-- fields -->
55 55
56 56 <div class="fields">
57 57 <div class="field">
58 58 <div class="label label-checkbox">
59 59 <label>${_('index build option')}:</label>
60 60 </div>
61 61 <div class="checkboxes">
62 62 <div class="checkbox">
63 63 ${h.checkbox('full_index',True)}
64 64 <label for="full_index">${_('build from scratch')}</label>
65 65 </div>
66 66 </div>
67 67 </div>
68 68
69 69 <div class="buttons">
70 70 ${h.submit('reindex',_('Reindex'),class_="ui-btn large")}
71 71 </div>
72 72 </div>
73 73 </div>
74 74 ${h.end_form()}
75 75
76 76 <h3>${_('Global application settings')}</h3>
77 77 ${h.form(url('admin_setting', setting_id='global'),method='put')}
78 78 <div class="form">
79 79 <!-- fields -->
80 80
81 81 <div class="fields">
82 82
83 83 <div class="field">
84 84 <div class="label">
85 85 <label for="rhodecode_title">${_('Application name')}:</label>
86 86 </div>
87 87 <div class="input">
88 88 ${h.text('rhodecode_title',size=30)}
89 89 </div>
90 90 </div>
91 91
92 92 <div class="field">
93 93 <div class="label">
94 94 <label for="rhodecode_realm">${_('Realm text')}:</label>
95 95 </div>
96 96 <div class="input">
97 97 ${h.text('rhodecode_realm',size=30)}
98 98 </div>
99 99 </div>
100 100
101 101 <div class="field">
102 102 <div class="label">
103 103 <label for="rhodecode_ga_code">${_('GA code')}:</label>
104 104 </div>
105 105 <div class="input">
106 106 ${h.text('rhodecode_ga_code',size=30)}
107 107 </div>
108 108 </div>
109 109
110 110 <div class="buttons">
111 111 ${h.submit('save',_('Save settings'),class_="ui-btn large")}
112 112 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
113 113 </div>
114 114 </div>
115 115 </div>
116 116 ${h.end_form()}
117 117
118 118 <h3>${_('VCS settings')}</h3>
119 119 ${h.form(url('admin_setting', setting_id='vcs'),method='put')}
120 120 <div class="form">
121 121 <!-- fields -->
122 122
123 123 <div class="fields">
124 124
125 125 <div class="field">
126 126 <div class="label label-checkbox">
127 127 <label>${_('Web')}:</label>
128 128 </div>
129 129 <div class="checkboxes">
130 130 <div class="checkbox">
131 131 ${h.checkbox('web_push_ssl','true')}
132 <label for="web_push_ssl">${_('require ssl for pushing')}</label>
132 <label for="web_push_ssl">${_('require ssl for vcs operations')}</label>
133 133 </div>
134 134 </div>
135 135 </div>
136 136
137 137 <div class="field">
138 138 <div class="label label-checkbox">
139 139 <label>${_('Hooks')}:</label>
140 140 </div>
141 141 <div class="checkboxes">
142 142 <div class="checkbox">
143 143 ${h.checkbox('hooks_changegroup_update','True')}
144 144 <label for="hooks_changegroup_update">${_('Update repository after push (hg update)')}</label>
145 145 </div>
146 146 <div class="checkbox">
147 147 ${h.checkbox('hooks_changegroup_repo_size','True')}
148 148 <label for="hooks_changegroup_repo_size">${_('Show repository size after push')}</label>
149 149 </div>
150 150 <div class="checkbox">
151 151 ${h.checkbox('hooks_changegroup_push_logger','True')}
152 152 <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label>
153 153 </div>
154 154 <div class="checkbox">
155 155 ${h.checkbox('hooks_preoutgoing_pull_logger','True')}
156 156 <label for="hooks_preoutgoing_pull_logger">${_('Log user pull commands')}</label>
157 157 </div>
158 158 </div>
159 159 <div class="input" style="margin-top:10px">
160 160 ${h.link_to(_('advanced setup'),url('admin_edit_setting',setting_id='hooks'),class_="ui-btn")}
161 161 </div>
162 162 </div>
163 163 <div class="field">
164 164 <div class="label">
165 165 <label for="paths_root_path">${_('Repositories location')}:</label>
166 166 </div>
167 167 <div class="input">
168 168 ${h.text('paths_root_path',size=30,readonly="readonly")}
169 169 <span id="path_unlock" class="tooltip"
170 170 title="${h.tooltip(_('This a crucial application setting. If you are really sure you need to change this, you must restart application in order to make this setting take effect. Click this label to unlock.'))}">
171 171 ${_('unlock')}</span>
172 172 </div>
173 173 </div>
174 174
175 175 <div class="buttons">
176 176 ${h.submit('save',_('Save settings'),class_="ui-btn large")}
177 177 ${h.reset('reset',_('Reset'),class_="ui-btn large")}
178 178 </div>
179 179 </div>
180 180 </div>
181 181 ${h.end_form()}
182 182
183 183 <script type="text/javascript">
184 184 YAHOO.util.Event.onDOMReady(function(){
185 185 YAHOO.util.Event.addListener('path_unlock','click',function(){
186 186 YAHOO.util.Dom.get('paths_root_path').removeAttribute('readonly');
187 187 });
188 188 });
189 189 </script>
190 190
191 191 <h3>${_('Test Email')}</h3>
192 192 ${h.form(url('admin_setting', setting_id='email'),method='put')}
193 193 <div class="form">
194 194 <!-- fields -->
195 195
196 196 <div class="fields">
197 197 <div class="field">
198 198 <div class="label">
199 199 <label for="test_email">${_('Email to')}:</label>
200 200 </div>
201 201 <div class="input">
202 202 ${h.text('test_email',size=30)}
203 203 </div>
204 204 </div>
205 205
206 206 <div class="buttons">
207 207 ${h.submit('send',_('Send'),class_="ui-btn large")}
208 208 </div>
209 209 </div>
210 210 </div>
211 211 ${h.end_form()}
212 212
213 213 <h3>${_('System Info and Packages')}</h3>
214 214 <div class="form">
215 215 <div>
216 216 <h5 id="expand_modules" style="cursor: pointer">&darr; ${_('show')} &darr;</h5>
217 217 </div>
218 218 <div id="expand_modules_table" style="display:none">
219 219 <h5>Python - ${c.py_version}</h5>
220 220 <h5>System - ${c.platform}</h5>
221 221
222 222 <table class="table" style="margin:0px 0px 0px 20px">
223 223 <colgroup>
224 224 <col style="width:220px">
225 225 </colgroup>
226 226 <tbody>
227 227 %for key, value in c.modules:
228 228 <tr>
229 229 <th style="text-align: right;padding-right:5px;">${key}</th>
230 230 <td>${value}</td>
231 231 </tr>
232 232 %endfor
233 233 </tbody>
234 234 </table>
235 235 </div>
236 236 </div>
237 237
238 238 <script type="text/javascript">
239 239 YUE.on('expand_modules','click',function(e){
240 240 YUD.setStyle('expand_modules_table','display','');
241 241 YUD.setStyle('expand_modules','display','none');
242 242 })
243 243 </script>
244 244
245 245 </div>
246 246 </%def>
General Comments 0
You need to be logged in to leave comments. Login now