##// END OF EJS Templates
Added full last changeset info to lightweight dashboard
marcink -
r3147:8182ebed beta
parent child Browse files
Show More
@@ -1,125 +1,133 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.home
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Home controller for Rhodecode
7 7
8 8 :created_on: Feb 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import tmpl_context as c, request
29 29 from pylons.i18n.translation import _
30 30 from webob.exc import HTTPBadRequest
31 31
32 32 import rhodecode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.auth import LoginRequired
36 36 from rhodecode.lib.base import BaseController, render
37 37 from rhodecode.model.db import Repository
38 38 from sqlalchemy.sql.expression import func
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class HomeController(BaseController):
44 44
45 45 @LoginRequired()
46 46 def __before__(self):
47 47 super(HomeController, self).__before__()
48 48
49 49 def index(self):
50 50 c.groups = self.scm_model.get_repos_groups()
51 51 c.group = None
52 52
53 53 if c.visual.lightweight_dashboard is False:
54 54 c.repos_list = self.scm_model.get_repos()
55 55 ## lightweight version of dashboard
56 56 else:
57 57 c.repos_list = Repository.query()\
58 58 .filter(Repository.group_id == None)\
59 59 .order_by(func.lower(Repository.repo_name))\
60 60 .all()
61 61 repos_data = []
62 62 total_records = len(c.repos_list)
63 63
64 64 _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
65 65 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
66 66
67 67 quick_menu = lambda repo_name: (template.get_def("quick_menu")
68 68 .render(repo_name, _=_, h=h, c=c))
69 69 repo_lnk = lambda name, rtype, private, fork_of: (
70 70 template.get_def("repo_name")
71 71 .render(name, rtype, private, fork_of, short_name=False,
72 72 admin=False, _=_, h=h, c=c))
73 73 last_change = lambda last_change: (template.get_def("last_change")
74 74 .render(last_change, _=_, h=h, c=c))
75 75 rss_lnk = lambda repo_name: (template.get_def("rss")
76 76 .render(repo_name, _=_, h=h, c=c))
77 77 atom_lnk = lambda repo_name: (template.get_def("atom")
78 78 .render(repo_name, _=_, h=h, c=c))
79 tip = lambda repo_name, cs_cache: (template.get_def("revision")
80 .render(repo_name,
81 cs_cache.get('revision'),
82 cs_cache.get('raw_id'),
83 cs_cache.get('author'),
84 cs_cache.get('message'), _=_, h=h,
85 c=c))
79 86
80 87 def desc(desc):
81 88 if c.visual.stylify_metatags:
82 89 return h.urlify_text(h.desc_stylize(h.truncate(desc, 60)))
83 90 else:
84 91 return h.urlify_text(h.truncate(desc, 60))
85 92
86 93 for repo in c.repos_list:
87 94 repos_data.append({
88 95 "menu": quick_menu(repo.repo_name),
89 96 "raw_name": repo.repo_name.lower(),
90 97 "name": repo_lnk(repo.repo_name, repo.repo_type,
91 98 repo.private, repo.fork),
92 99 "last_change": last_change(repo.last_db_change),
100 "tip": tip(repo.repo_name, repo.changeset_cache),
93 101 "desc": desc(repo.description),
94 102 "owner": h.person(repo.user.username),
95 103 "rss": rss_lnk(repo.repo_name),
96 104 "atom": atom_lnk(repo.repo_name),
97 105 })
98 106
99 107 c.data = json.dumps({
100 108 "totalRecords": total_records,
101 109 "startIndex": 0,
102 110 "sort": "name",
103 111 "dir": "asc",
104 112 "records": repos_data
105 113 })
106 114
107 115 return render('/index.html')
108 116
109 117 def repo_switcher(self):
110 118 if request.is_xhr:
111 119 all_repos = Repository.query().order_by(Repository.repo_name).all()
112 120 c.repos_list = self.scm_model.get_repos(all_repos,
113 121 sort_key='name_sort',
114 122 simple=True)
115 123 return render('/repo_switcher_list.html')
116 124 else:
117 125 raise HTTPBadRequest()
118 126
119 127 def branch_tag_switcher(self, repo_name):
120 128 if request.is_xhr:
121 129 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
122 130 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
123 131 return render('/switch_to_list.html')
124 132 else:
125 133 raise HTTPBadRequest()
@@ -1,332 +1,332 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, AUTHORIZATION
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, AttributeDict,\
21 21 safe_str, safe_int
22 22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
23 23 HasPermissionAnyMiddleware, CookieStoreWrapper
24 24 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
25 25 from rhodecode.model import meta
26 26
27 27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
28 28 from rhodecode.model.notification import NotificationModel
29 29 from rhodecode.model.scm import ScmModel
30 30 from rhodecode.model.meta import Session
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 def _get_ip_addr(environ):
36 36 proxy_key = 'HTTP_X_REAL_IP'
37 37 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
38 38 def_key = 'REMOTE_ADDR'
39 39
40 40 ip = environ.get(proxy_key2)
41 41 if ip:
42 42 return ip
43 43
44 44 ip = environ.get(proxy_key)
45 45
46 46 if ip:
47 47 return ip
48 48
49 49 ip = environ.get(def_key, '0.0.0.0')
50 50 return ip
51 51
52 52
53 53 def _get_access_path(environ):
54 54 path = environ.get('PATH_INFO')
55 55 org_req = environ.get('pylons.original_request')
56 56 if org_req:
57 57 path = org_req.environ.get('PATH_INFO')
58 58 return path
59 59
60 60
61 61 class BasicAuth(AuthBasicAuthenticator):
62 62
63 63 def __init__(self, realm, authfunc, auth_http_code=None):
64 64 self.realm = realm
65 65 self.authfunc = authfunc
66 66 self._rc_auth_http_code = auth_http_code
67 67
68 68 def build_authentication(self):
69 69 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
70 70 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
71 71 # return 403 if alternative http return code is specified in
72 72 # RhodeCode config
73 73 return HTTPForbidden(headers=head)
74 74 return HTTPUnauthorized(headers=head)
75 75
76 76 def authenticate(self, environ):
77 77 authorization = AUTHORIZATION(environ)
78 78 if not authorization:
79 79 return self.build_authentication()
80 80 (authmeth, auth) = authorization.split(' ', 1)
81 81 if 'basic' != authmeth.lower():
82 82 return self.build_authentication()
83 83 auth = auth.strip().decode('base64')
84 84 _parts = auth.split(':', 1)
85 85 if len(_parts) == 2:
86 86 username, password = _parts
87 87 if self.authfunc(environ, username, password):
88 88 return username
89 89 return self.build_authentication()
90 90
91 91 __call__ = authenticate
92 92
93 93
94 94 class BaseVCSController(object):
95 95
96 96 def __init__(self, application, config):
97 97 self.application = application
98 98 self.config = config
99 99 # base path of repo locations
100 100 self.basepath = self.config['base_path']
101 101 #authenticate this mercurial request using authfunc
102 102 self.authenticate = BasicAuth('', authfunc,
103 103 config.get('auth_ret_code'))
104 104 self.ip_addr = '0.0.0.0'
105 105
106 106 def _handle_request(self, environ, start_response):
107 107 raise NotImplementedError()
108 108
109 109 def _get_by_id(self, repo_name):
110 110 """
111 111 Get's a special pattern _<ID> from clone url and tries to replace it
112 112 with a repository_name for support of _<ID> non changable urls
113 113
114 114 :param repo_name:
115 115 """
116 116 try:
117 117 data = repo_name.split('/')
118 118 if len(data) >= 2:
119 119 by_id = data[1].split('_')
120 120 if len(by_id) == 2 and by_id[1].isdigit():
121 121 _repo_name = Repository.get(by_id[1]).repo_name
122 122 data[1] = _repo_name
123 123 except:
124 124 log.debug('Failed to extract repo_name from id %s' % (
125 125 traceback.format_exc()
126 126 )
127 127 )
128 128
129 129 return '/'.join(data)
130 130
131 131 def _invalidate_cache(self, repo_name):
132 132 """
133 133 Set's cache for this repository for invalidation on next access
134 134
135 135 :param repo_name: full repo name, also a cache key
136 136 """
137 137 invalidate_cache('get_repo_cached_%s' % repo_name)
138 138
139 139 def _check_permission(self, action, user, repo_name, ip_addr=None):
140 140 """
141 141 Checks permissions using action (push/pull) user and repository
142 142 name
143 143
144 144 :param action: push or pull action
145 145 :param user: user instance
146 146 :param repo_name: repository name
147 147 """
148 148 #check IP
149 149 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
150 150 if not authuser.ip_allowed:
151 151 return False
152 152 else:
153 153 log.info('Access for IP:%s allowed' % (ip_addr))
154 154 if action == 'push':
155 155 if not HasPermissionAnyMiddleware('repository.write',
156 156 'repository.admin')(user,
157 157 repo_name):
158 158 return False
159 159
160 160 else:
161 161 #any other action need at least read permission
162 162 if not HasPermissionAnyMiddleware('repository.read',
163 163 'repository.write',
164 164 'repository.admin')(user,
165 165 repo_name):
166 166 return False
167 167
168 168 return True
169 169
170 170 def _get_ip_addr(self, environ):
171 171 return _get_ip_addr(environ)
172 172
173 173 def _check_ssl(self, environ, start_response):
174 174 """
175 175 Checks the SSL check flag and returns False if SSL is not present
176 176 and required True otherwise
177 177 """
178 178 org_proto = environ['wsgi._org_proto']
179 179 #check if we have SSL required ! if not it's a bad request !
180 180 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
181 181 if require_ssl and org_proto == 'http':
182 182 log.debug('proto is %s and SSL is required BAD REQUEST !'
183 183 % org_proto)
184 184 return False
185 185 return True
186 186
187 187 def _check_locking_state(self, environ, action, repo, user_id):
188 188 """
189 189 Checks locking on this repository, if locking is enabled and lock is
190 190 present returns a tuple of make_lock, locked, locked_by.
191 191 make_lock can have 3 states None (do nothing) True, make lock
192 192 False release lock, This value is later propagated to hooks, which
193 193 do the locking. Think about this as signals passed to hooks what to do.
194 194
195 195 """
196 196 locked = False # defines that locked error should be thrown to user
197 197 make_lock = None
198 198 repo = Repository.get_by_repo_name(repo)
199 199 user = User.get(user_id)
200 200
201 201 # this is kind of hacky, but due to how mercurial handles client-server
202 202 # server see all operation on changeset; bookmarks, phases and
203 203 # obsolescence marker in different transaction, we don't want to check
204 204 # locking on those
205 205 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
206 206 locked_by = repo.locked
207 207 if repo and repo.enable_locking and not obsolete_call:
208 208 if action == 'push':
209 209 #check if it's already locked !, if it is compare users
210 210 user_id, _date = repo.locked
211 211 if user.user_id == user_id:
212 212 log.debug('Got push from user %s, now unlocking' % (user))
213 213 # unlock if we have push from user who locked
214 214 make_lock = False
215 215 else:
216 216 # we're not the same user who locked, ban with 423 !
217 217 locked = True
218 218 if action == 'pull':
219 219 if repo.locked[0] and repo.locked[1]:
220 220 locked = True
221 221 else:
222 222 log.debug('Setting lock on repo %s by %s' % (repo, user))
223 223 make_lock = True
224 224
225 225 else:
226 226 log.debug('Repository %s do not have locking enabled' % (repo))
227 227 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
228 228 % (make_lock, locked, locked_by))
229 229 return make_lock, locked, locked_by
230 230
231 231 def __call__(self, environ, start_response):
232 232 start = time.time()
233 233 try:
234 234 return self._handle_request(environ, start_response)
235 235 finally:
236 236 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
237 237 log.debug('Request time: %.3fs' % (time.time() - start))
238 238 meta.Session.remove()
239 239
240 240
241 241 class BaseController(WSGIController):
242 242
243 243 def __before__(self):
244 244 """
245 245 __before__ is called before controller methods and after __call__
246 246 """
247 247 c.rhodecode_version = __version__
248 248 c.rhodecode_instanceid = config.get('instance_id')
249 249 c.rhodecode_name = config.get('rhodecode_title')
250 250 c.use_gravatar = str2bool(config.get('use_gravatar'))
251 251 c.ga_code = config.get('rhodecode_ga_code')
252 252 # Visual options
253 253 c.visual = AttributeDict({})
254 254 rc_config = RhodeCodeSetting.get_app_settings()
255 255
256 256 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
257 257 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
258 258 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
259 259 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
260 260 c.visual.lightweight_dashboard_items = safe_int(config.get('dashboard_items', 100))
261 261
262 262 c.repo_name = get_repo_slug(request)
263 263 c.backends = BACKENDS.keys()
264 264 c.unread_notifications = NotificationModel()\
265 265 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
266 266 self.cut_off_limit = int(config.get('cut_off_limit'))
267 267
268 268 self.sa = meta.Session
269 269 self.scm_model = ScmModel(self.sa)
270 270
271 271 def __call__(self, environ, start_response):
272 272 """Invoke the Controller"""
273 273 # WSGIController.__call__ dispatches to the Controller method
274 274 # the request is routed to. This routing information is
275 275 # available in environ['pylons.routes_dict']
276 276 start = time.time()
277 277 try:
278 278 self.ip_addr = _get_ip_addr(environ)
279 279 # make sure that we update permissions each time we call controller
280 280 api_key = request.GET.get('api_key')
281 281 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
282 282 user_id = cookie_store.get('user_id', None)
283 283 username = get_container_username(environ, config)
284 284 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
285 285 request.user = auth_user
286 286 self.rhodecode_user = c.rhodecode_user = auth_user
287 287 if not self.rhodecode_user.is_authenticated and \
288 288 self.rhodecode_user.user_id is not None:
289 289 self.rhodecode_user.set_authenticated(
290 290 cookie_store.get('is_authenticated')
291 291 )
292 292 log.info('IP: %s User: %s accessed %s' % (
293 293 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
294 294 )
295 295 return WSGIController.__call__(self, environ, start_response)
296 296 finally:
297 297 log.info('IP: %s Request to %s time: %.3fs' % (
298 298 _get_ip_addr(environ),
299 299 safe_unicode(_get_access_path(environ)), time.time() - start)
300 300 )
301 301 meta.Session.remove()
302 302
303 303
304 304 class BaseRepoController(BaseController):
305 305 """
306 306 Base class for controllers responsible for loading all needed data for
307 307 repository loaded items are
308 308
309 309 c.rhodecode_repo: instance of scm repository
310 310 c.rhodecode_db_repo: instance of db
311 311 c.repository_followers: number of followers
312 312 c.repository_forks: number of forks
313 313 """
314 314
315 315 def __before__(self):
316 316 super(BaseRepoController, self).__before__()
317 317 if c.repo_name:
318 318
319 319 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
320 320 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
321 321 # update last change according to VCS data
322 dbr.update_last_change(c.rhodecode_repo.last_change)
322 dbr.update_changeset_cache(dbr.get_changeset())
323 323 if c.rhodecode_repo is None:
324 324 log.error('%s this repository is present in database but it '
325 325 'cannot be created as an scm instance', c.repo_name)
326 326
327 327 redirect(url('home'))
328 328
329 329 # some globals counter for menu
330 330 c.repository_followers = self.scm_model.get_followers(dbr)
331 331 c.repository_forks = self.scm_model.get_forks(dbr)
332 332 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
@@ -1,34 +1,51 b''
1 1 import logging
2 2 import datetime
3 3
4 4 from sqlalchemy import *
5 5 from sqlalchemy.exc import DatabaseError
6 6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 7 from sqlalchemy.orm.session import Session
8 8 from sqlalchemy.ext.declarative import declarative_base
9 9
10 10 from rhodecode.lib.dbmigrate.migrate import *
11 11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 12
13 13 from rhodecode.model.meta import Base
14 14 from rhodecode.model import meta
15 15
16 16 log = logging.getLogger(__name__)
17 17
18 18
19 19 def upgrade(migrate_engine):
20 20 """
21 21 Upgrade operations go here.
22 22 Don't create your own engine; bind migrate_engine to your metadata
23 23 """
24 24 #==========================================================================
25 25 # USER LOGS
26 26 #==========================================================================
27 27 from rhodecode.lib.dbmigrate.schema.db_1_5_0 import UserIpMap
28 28 tbl = UserIpMap.__table__
29 29 tbl.create()
30 30
31 #==========================================================================
32 # REPOSITORIES
33 #==========================================================================
34 from rhodecode.lib.dbmigrate.schema.db_1_5_0 import Repository
35 tbl = Repository.__table__
36 changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True)
37 # create username column
38 changeset_cache.create(table=tbl)
39
40 #fix cache data
41 _Session = Session()
42 ## after adding that column fix all usernames
43 repositories = _Session.query(Repository).all()
44 for entry in repositories:
45 entry.update_changeset_cache()
46 _Session.commit()
47
31 48
32 49 def downgrade(migrate_engine):
33 50 meta = MetaData()
34 51 meta.bind = migrate_engine
@@ -1,86 +1,87 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 package.rhodecode.lib.cleanup
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 :created_on: Jul 14, 2012
7 7 :author: marcink
8 8 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
9 9 :license: GPLv3, see COPYING for more details.
10 10 """
11 11 # This program is free software: you can redistribute it and/or modify
12 12 # it under the terms of the GNU General Public License as published by
13 13 # the Free Software Foundation, either version 3 of the License, or
14 14 # (at your option) any later version.
15 15 #
16 16 # This program is distributed in the hope that it will be useful,
17 17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 19 # GNU General Public License for more details.
20 20 #
21 21 # You should have received a copy of the GNU General Public License
22 22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 23 from __future__ import with_statement
24 24
25 25 import os
26 26 import sys
27 27 import re
28 28 import shutil
29 29 import logging
30 30 import datetime
31 31 import string
32 32
33 33 from os.path import dirname as dn, join as jn
34 34 from rhodecode.model import init_model
35 35 from rhodecode.lib.utils2 import engine_from_config, safe_str
36 36 from rhodecode.model.db import RhodeCodeUi, Repository
37 from rhodecode.lib.vcs.backends.base import EmptyChangeset
37 38
38 39
39 40 #to get the rhodecode import
40 41 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
41 42
42 43 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache
43 44
44 45 log = logging.getLogger(__name__)
45 46
46 47
47 48 class UpdateCommand(BasePasterCommand):
48 49
49 50 max_args = 1
50 51 min_args = 1
51 52
52 53 usage = "CONFIG_FILE"
53 54 summary = "Cleanup deleted repos"
54 55 group_name = "RhodeCode"
55 56 takes_config_file = -1
56 57 parser = Command.standard_parser(verbose=True)
57 58
58 59 def command(self):
59 60 logging.config.fileConfig(self.path_to_ini_file)
60 61 from pylons import config
61 62
62 63 #get to remove repos !!
63 64 add_cache(config)
64 65 engine = engine_from_config(config, 'sqlalchemy.db1.')
65 66 init_model(engine)
66 67
67 68 repo_update_list = map(string.strip,
68 69 self.options.repo_update_list.split(',')) \
69 70 if self.options.repo_update_list else None
70 71
71 72 if repo_update_list:
72 73 repo_list = Repository.query().filter(Repository.repo_name.in_(repo_update_list))
73 74 else:
74 75 repo_list = Repository.getAll()
75 76 for repo in repo_list:
76 last_change = (repo.scm_instance.last_change if repo.scm_instance
77 else datetime.datetime.utcfromtimestamp(0))
78 repo.update_last_change(last_change)
77 last_cs = (repo.scm_instance.get_changeset() if repo.scm_instance
78 else EmptyChangeset())
79 repo.update_changeset_cache(last_cs)
79 80
80 81 def update_parser(self):
81 82 self.parser.add_option('--update-only',
82 83 action='store',
83 84 dest='repo_update_list',
84 85 help="Specifies a comma separated list of repositores "
85 86 "to update last commit info for. OPTIONAL",
86 87 )
@@ -1,753 +1,754 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 import decorator
36 36 import warnings
37 37 from os.path import abspath
38 38 from os.path import dirname as dn, join as jn
39 39
40 40 from paste.script.command import Command, BadCommand
41 41
42 42 from mercurial import ui, config
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45
46 46 from rhodecode.lib.vcs import get_backend
47 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model import meta
55 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
56 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.repos_group import ReposGroupModel
59 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
60 60 from rhodecode.lib.vcs.utils.fakemod import create_module
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
65 65
66 66
67 67 def recursive_replace(str_, replace=' '):
68 68 """
69 69 Recursive replace of given sign to just one instance
70 70
71 71 :param str_: given string
72 72 :param replace: char to find and replace multiple instances
73 73
74 74 Examples::
75 75 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
76 76 'Mighty-Mighty-Bo-sstones'
77 77 """
78 78
79 79 if str_.find(replace * 2) == -1:
80 80 return str_
81 81 else:
82 82 str_ = str_.replace(replace * 2, replace)
83 83 return recursive_replace(str_, replace)
84 84
85 85
86 86 def repo_name_slug(value):
87 87 """
88 88 Return slug of name of repository
89 89 This function is called on each creation/modification
90 90 of repository to prevent bad names in repo
91 91 """
92 92
93 93 slug = remove_formatting(value)
94 94 slug = strip_tags(slug)
95 95
96 96 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
97 97 slug = slug.replace(c, '-')
98 98 slug = recursive_replace(slug, '-')
99 99 slug = collapse(slug, '-')
100 100 return slug
101 101
102 102
103 103 def get_repo_slug(request):
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105 if _repo:
106 106 _repo = _repo.rstrip('/')
107 107 return _repo
108 108
109 109
110 110 def get_repos_group_slug(request):
111 111 _group = request.environ['pylons.routes_dict'].get('group_name')
112 112 if _group:
113 113 _group = _group.rstrip('/')
114 114 return _group
115 115
116 116
117 117 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
118 118 """
119 119 Action logger for various actions made by users
120 120
121 121 :param user: user that made this action, can be a unique username string or
122 122 object containing user_id attribute
123 123 :param action: action to log, should be on of predefined unique actions for
124 124 easy translations
125 125 :param repo: string name of repository or object containing repo_id,
126 126 that action was made on
127 127 :param ipaddr: optional ip address from what the action was made
128 128 :param sa: optional sqlalchemy session
129 129
130 130 """
131 131
132 132 if not sa:
133 133 sa = meta.Session()
134 134
135 135 try:
136 136 if hasattr(user, 'user_id'):
137 137 user_obj = User.get(user.user_id)
138 138 elif isinstance(user, basestring):
139 139 user_obj = User.get_by_username(user)
140 140 else:
141 141 raise Exception('You have to provide a user object or a username')
142 142
143 143 if hasattr(repo, 'repo_id'):
144 144 repo_obj = Repository.get(repo.repo_id)
145 145 repo_name = repo_obj.repo_name
146 146 elif isinstance(repo, basestring):
147 147 repo_name = repo.lstrip('/')
148 148 repo_obj = Repository.get_by_repo_name(repo_name)
149 149 else:
150 150 repo_obj = None
151 151 repo_name = ''
152 152
153 153 user_log = UserLog()
154 154 user_log.user_id = user_obj.user_id
155 155 user_log.username = user_obj.username
156 156 user_log.action = safe_unicode(action)
157 157
158 158 user_log.repository = repo_obj
159 159 user_log.repository_name = repo_name
160 160
161 161 user_log.action_date = datetime.datetime.now()
162 162 user_log.user_ip = ipaddr
163 163 sa.add(user_log)
164 164
165 165 log.info('Logging action %s on %s by %s' %
166 166 (action, safe_unicode(repo), user_obj))
167 167 if commit:
168 168 sa.commit()
169 169 except:
170 170 log.error(traceback.format_exc())
171 171 raise
172 172
173 173
174 174 def get_repos(path, recursive=False):
175 175 """
176 176 Scans given path for repos and return (name,(type,path)) tuple
177 177
178 178 :param path: path to scan for repositories
179 179 :param recursive: recursive search and return names with subdirs in front
180 180 """
181 181
182 182 # remove ending slash for better results
183 183 path = path.rstrip(os.sep)
184 184
185 185 def _get_repos(p):
186 186 if not os.access(p, os.W_OK):
187 187 return
188 188 for dirpath in os.listdir(p):
189 189 if os.path.isfile(os.path.join(p, dirpath)):
190 190 continue
191 191 cur_path = os.path.join(p, dirpath)
192 192 try:
193 193 scm_info = get_scm(cur_path)
194 194 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
195 195 except VCSError:
196 196 if not recursive:
197 197 continue
198 198 #check if this dir containts other repos for recursive scan
199 199 rec_path = os.path.join(p, dirpath)
200 200 if os.path.isdir(rec_path):
201 201 for inner_scm in _get_repos(rec_path):
202 202 yield inner_scm
203 203
204 204 return _get_repos(path)
205 205
206 206
207 207 def is_valid_repo(repo_name, base_path, scm=None):
208 208 """
209 209 Returns True if given path is a valid repository False otherwise.
210 210 If scm param is given also compare if given scm is the same as expected
211 211 from scm parameter
212 212
213 213 :param repo_name:
214 214 :param base_path:
215 215 :param scm:
216 216
217 217 :return True: if given path is a valid repository
218 218 """
219 219 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
220 220
221 221 try:
222 222 scm_ = get_scm(full_path)
223 223 if scm:
224 224 return scm_[0] == scm
225 225 return True
226 226 except VCSError:
227 227 return False
228 228
229 229
230 230 def is_valid_repos_group(repos_group_name, base_path):
231 231 """
232 232 Returns True if given path is a repos group False otherwise
233 233
234 234 :param repo_name:
235 235 :param base_path:
236 236 """
237 237 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
238 238
239 239 # check if it's not a repo
240 240 if is_valid_repo(repos_group_name, base_path):
241 241 return False
242 242
243 243 try:
244 244 # we need to check bare git repos at higher level
245 245 # since we might match branches/hooks/info/objects or possible
246 246 # other things inside bare git repo
247 247 get_scm(os.path.dirname(full_path))
248 248 return False
249 249 except VCSError:
250 250 pass
251 251
252 252 # check if it's a valid path
253 253 if os.path.isdir(full_path):
254 254 return True
255 255
256 256 return False
257 257
258 258
259 259 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
260 260 while True:
261 261 ok = raw_input(prompt)
262 262 if ok in ('y', 'ye', 'yes'):
263 263 return True
264 264 if ok in ('n', 'no', 'nop', 'nope'):
265 265 return False
266 266 retries = retries - 1
267 267 if retries < 0:
268 268 raise IOError
269 269 print complaint
270 270
271 271 #propagated from mercurial documentation
272 272 ui_sections = ['alias', 'auth',
273 273 'decode/encode', 'defaults',
274 274 'diff', 'email',
275 275 'extensions', 'format',
276 276 'merge-patterns', 'merge-tools',
277 277 'hooks', 'http_proxy',
278 278 'smtp', 'patch',
279 279 'paths', 'profiling',
280 280 'server', 'trusted',
281 281 'ui', 'web', ]
282 282
283 283
284 284 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
285 285 """
286 286 A function that will read python rc files or database
287 287 and make an mercurial ui object from read options
288 288
289 289 :param path: path to mercurial config file
290 290 :param checkpaths: check the path
291 291 :param read_from: read from 'file' or 'db'
292 292 """
293 293
294 294 baseui = ui.ui()
295 295
296 296 # clean the baseui object
297 297 baseui._ocfg = config.config()
298 298 baseui._ucfg = config.config()
299 299 baseui._tcfg = config.config()
300 300
301 301 if read_from == 'file':
302 302 if not os.path.isfile(path):
303 303 log.debug('hgrc file is not present at %s, skipping...' % path)
304 304 return False
305 305 log.debug('reading hgrc from %s' % path)
306 306 cfg = config.config()
307 307 cfg.read(path)
308 308 for section in ui_sections:
309 309 for k, v in cfg.items(section):
310 310 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
311 311 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
312 312
313 313 elif read_from == 'db':
314 314 sa = meta.Session()
315 315 ret = sa.query(RhodeCodeUi)\
316 316 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
317 317 .all()
318 318
319 319 hg_ui = ret
320 320 for ui_ in hg_ui:
321 321 if ui_.ui_active:
322 322 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
323 323 ui_.ui_key, ui_.ui_value)
324 324 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
325 325 safe_str(ui_.ui_value))
326 326 if ui_.ui_key == 'push_ssl':
327 327 # force set push_ssl requirement to False, rhodecode
328 328 # handles that
329 329 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
330 330 False)
331 331 if clear_session:
332 332 meta.Session.remove()
333 333 return baseui
334 334
335 335
336 336 def set_rhodecode_config(config):
337 337 """
338 338 Updates pylons config with new settings from database
339 339
340 340 :param config:
341 341 """
342 342 hgsettings = RhodeCodeSetting.get_app_settings()
343 343
344 344 for k, v in hgsettings.items():
345 345 config[k] = v
346 346
347 347
348 348 def invalidate_cache(cache_key, *args):
349 349 """
350 350 Puts cache invalidation task into db for
351 351 further global cache invalidation
352 352 """
353 353
354 354 from rhodecode.model.scm import ScmModel
355 355
356 356 if cache_key.startswith('get_repo_cached_'):
357 357 name = cache_key.split('get_repo_cached_')[-1]
358 358 ScmModel().mark_for_invalidation(name)
359 359
360 360
361 361 def map_groups(path):
362 362 """
363 363 Given a full path to a repository, create all nested groups that this
364 364 repo is inside. This function creates parent-child relationships between
365 365 groups and creates default perms for all new groups.
366 366
367 367 :param paths: full path to repository
368 368 """
369 369 sa = meta.Session()
370 370 groups = path.split(Repository.url_sep())
371 371 parent = None
372 372 group = None
373 373
374 374 # last element is repo in nested groups structure
375 375 groups = groups[:-1]
376 376 rgm = ReposGroupModel(sa)
377 377 for lvl, group_name in enumerate(groups):
378 378 group_name = '/'.join(groups[:lvl] + [group_name])
379 379 group = RepoGroup.get_by_group_name(group_name)
380 380 desc = '%s group' % group_name
381 381
382 382 # skip folders that are now removed repos
383 383 if REMOVED_REPO_PAT.match(group_name):
384 384 break
385 385
386 386 if group is None:
387 387 log.debug('creating group level: %s group_name: %s' % (lvl,
388 388 group_name))
389 389 group = RepoGroup(group_name, parent)
390 390 group.group_description = desc
391 391 sa.add(group)
392 392 rgm._create_default_perms(group)
393 393 sa.flush()
394 394 parent = group
395 395 return group
396 396
397 397
398 398 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
399 399 install_git_hook=False):
400 400 """
401 401 maps all repos given in initial_repo_list, non existing repositories
402 402 are created, if remove_obsolete is True it also check for db entries
403 403 that are not in initial_repo_list and removes them.
404 404
405 405 :param initial_repo_list: list of repositories found by scanning methods
406 406 :param remove_obsolete: check for obsolete entries in database
407 407 :param install_git_hook: if this is True, also check and install githook
408 408 for a repo if missing
409 409 """
410 410 from rhodecode.model.repo import RepoModel
411 411 from rhodecode.model.scm import ScmModel
412 412 sa = meta.Session()
413 413 rm = RepoModel()
414 414 user = sa.query(User).filter(User.admin == True).first()
415 415 if user is None:
416 416 raise Exception('Missing administrative account!')
417 417 added = []
418 418
419 419 # # clear cache keys
420 420 # log.debug("Clearing cache keys now...")
421 421 # CacheInvalidation.clear_cache()
422 422 # sa.commit()
423 423
424 424 ##creation defaults
425 425 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
426 426 enable_statistics = defs.get('repo_enable_statistics')
427 427 enable_locking = defs.get('repo_enable_locking')
428 428 enable_downloads = defs.get('repo_enable_downloads')
429 429 private = defs.get('repo_private')
430 430
431 431 for name, repo in initial_repo_list.items():
432 432 group = map_groups(name)
433 433 db_repo = rm.get_by_repo_name(name)
434 434 # found repo that is on filesystem not in RhodeCode database
435 435 if not db_repo:
436 436 log.info('repository %s not found, creating now' % name)
437 437 added.append(name)
438 438 desc = (repo.description
439 439 if repo.description != 'unknown'
440 440 else '%s repository' % name)
441 441
442 442 new_repo = rm.create_repo(
443 443 repo_name=name,
444 444 repo_type=repo.alias,
445 445 description=desc,
446 446 repos_group=getattr(group, 'group_id', None),
447 447 owner=user,
448 448 just_db=True,
449 449 enable_locking=enable_locking,
450 450 enable_downloads=enable_downloads,
451 451 enable_statistics=enable_statistics,
452 452 private=private
453 453 )
454 454 # we added that repo just now, and make sure it has githook
455 455 # installed
456 456 if new_repo.repo_type == 'git':
457 457 ScmModel().install_git_hook(new_repo.scm_instance)
458 new_repo.update_changeset_cache()
458 459 elif install_git_hook:
459 460 if db_repo.repo_type == 'git':
460 461 ScmModel().install_git_hook(db_repo.scm_instance)
461 462 # during starting install all cache keys for all repositories in the
462 463 # system, this will register all repos and multiple instances
463 464 key, _prefix, _org_key = CacheInvalidation._get_key(name)
464 465 CacheInvalidation.invalidate(name)
465 466 log.debug("Creating a cache key for %s, instance_id %s"
466 467 % (name, _prefix or 'unknown'))
467 468
468 469 sa.commit()
469 470 removed = []
470 471 if remove_obsolete:
471 472 # remove from database those repositories that are not in the filesystem
472 473 for repo in sa.query(Repository).all():
473 474 if repo.repo_name not in initial_repo_list.keys():
474 475 log.debug("Removing non-existing repository found in db `%s`" %
475 476 repo.repo_name)
476 477 try:
477 478 sa.delete(repo)
478 479 sa.commit()
479 480 removed.append(repo.repo_name)
480 481 except:
481 482 #don't hold further removals on error
482 483 log.error(traceback.format_exc())
483 484 sa.rollback()
484 485
485 486 return added, removed
486 487
487 488
488 489 # set cache regions for beaker so celery can utilise it
489 490 def add_cache(settings):
490 491 cache_settings = {'regions': None}
491 492 for key in settings.keys():
492 493 for prefix in ['beaker.cache.', 'cache.']:
493 494 if key.startswith(prefix):
494 495 name = key.split(prefix)[1].strip()
495 496 cache_settings[name] = settings[key].strip()
496 497 if cache_settings['regions']:
497 498 for region in cache_settings['regions'].split(','):
498 499 region = region.strip()
499 500 region_settings = {}
500 501 for key, value in cache_settings.items():
501 502 if key.startswith(region):
502 503 region_settings[key.split('.')[1]] = value
503 504 region_settings['expire'] = int(region_settings.get('expire',
504 505 60))
505 506 region_settings.setdefault('lock_dir',
506 507 cache_settings.get('lock_dir'))
507 508 region_settings.setdefault('data_dir',
508 509 cache_settings.get('data_dir'))
509 510
510 511 if 'type' not in region_settings:
511 512 region_settings['type'] = cache_settings.get('type',
512 513 'memory')
513 514 beaker.cache.cache_regions[region] = region_settings
514 515
515 516
516 517 def load_rcextensions(root_path):
517 518 import rhodecode
518 519 from rhodecode.config import conf
519 520
520 521 path = os.path.join(root_path, 'rcextensions', '__init__.py')
521 522 if os.path.isfile(path):
522 523 rcext = create_module('rc', path)
523 524 EXT = rhodecode.EXTENSIONS = rcext
524 525 log.debug('Found rcextensions now loading %s...' % rcext)
525 526
526 527 # Additional mappings that are not present in the pygments lexers
527 528 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
528 529
529 530 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
530 531
531 532 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
532 533 log.debug('settings custom INDEX_EXTENSIONS')
533 534 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
534 535
535 536 #ADDITIONAL MAPPINGS
536 537 log.debug('adding extra into INDEX_EXTENSIONS')
537 538 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
538 539
539 540
540 541 #==============================================================================
541 542 # TEST FUNCTIONS AND CREATORS
542 543 #==============================================================================
543 544 def create_test_index(repo_location, config, full_index):
544 545 """
545 546 Makes default test index
546 547
547 548 :param config: test config
548 549 :param full_index:
549 550 """
550 551
551 552 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
552 553 from rhodecode.lib.pidlock import DaemonLock, LockHeld
553 554
554 555 repo_location = repo_location
555 556
556 557 index_location = os.path.join(config['app_conf']['index_dir'])
557 558 if not os.path.exists(index_location):
558 559 os.makedirs(index_location)
559 560
560 561 try:
561 562 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
562 563 WhooshIndexingDaemon(index_location=index_location,
563 564 repo_location=repo_location)\
564 565 .run(full_index=full_index)
565 566 l.release()
566 567 except LockHeld:
567 568 pass
568 569
569 570
570 571 def create_test_env(repos_test_path, config):
571 572 """
572 573 Makes a fresh database and
573 574 install test repository into tmp dir
574 575 """
575 576 from rhodecode.lib.db_manage import DbManage
576 577 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
577 578
578 579 # PART ONE create db
579 580 dbconf = config['sqlalchemy.db1.url']
580 581 log.debug('making test db %s' % dbconf)
581 582
582 583 # create test dir if it doesn't exist
583 584 if not os.path.isdir(repos_test_path):
584 585 log.debug('Creating testdir %s' % repos_test_path)
585 586 os.makedirs(repos_test_path)
586 587
587 588 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
588 589 tests=True)
589 590 dbmanage.create_tables(override=True)
590 591 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
591 592 dbmanage.create_default_user()
592 593 dbmanage.admin_prompt()
593 594 dbmanage.create_permissions()
594 595 dbmanage.populate_default_permissions()
595 596 Session().commit()
596 597 # PART TWO make test repo
597 598 log.debug('making test vcs repositories')
598 599
599 600 idx_path = config['app_conf']['index_dir']
600 601 data_path = config['app_conf']['cache_dir']
601 602
602 603 #clean index and data
603 604 if idx_path and os.path.exists(idx_path):
604 605 log.debug('remove %s' % idx_path)
605 606 shutil.rmtree(idx_path)
606 607
607 608 if data_path and os.path.exists(data_path):
608 609 log.debug('remove %s' % data_path)
609 610 shutil.rmtree(data_path)
610 611
611 612 #CREATE DEFAULT TEST REPOS
612 613 cur_dir = dn(dn(abspath(__file__)))
613 614 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
614 615 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
615 616 tar.close()
616 617
617 618 cur_dir = dn(dn(abspath(__file__)))
618 619 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
619 620 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
620 621 tar.close()
621 622
622 623 #LOAD VCS test stuff
623 624 from rhodecode.tests.vcs import setup_package
624 625 setup_package()
625 626
626 627
627 628 #==============================================================================
628 629 # PASTER COMMANDS
629 630 #==============================================================================
630 631 class BasePasterCommand(Command):
631 632 """
632 633 Abstract Base Class for paster commands.
633 634
634 635 The celery commands are somewhat aggressive about loading
635 636 celery.conf, and since our module sets the `CELERY_LOADER`
636 637 environment variable to our loader, we have to bootstrap a bit and
637 638 make sure we've had a chance to load the pylons config off of the
638 639 command line, otherwise everything fails.
639 640 """
640 641 min_args = 1
641 642 min_args_error = "Please provide a paster config file as an argument."
642 643 takes_config_file = 1
643 644 requires_config_file = True
644 645
645 646 def notify_msg(self, msg, log=False):
646 647 """Make a notification to user, additionally if logger is passed
647 648 it logs this action using given logger
648 649
649 650 :param msg: message that will be printed to user
650 651 :param log: logging instance, to use to additionally log this message
651 652
652 653 """
653 654 if log and isinstance(log, logging):
654 655 log(msg)
655 656
656 657 def run(self, args):
657 658 """
658 659 Overrides Command.run
659 660
660 661 Checks for a config file argument and loads it.
661 662 """
662 663 if len(args) < self.min_args:
663 664 raise BadCommand(
664 665 self.min_args_error % {'min_args': self.min_args,
665 666 'actual_args': len(args)})
666 667
667 668 # Decrement because we're going to lob off the first argument.
668 669 # @@ This is hacky
669 670 self.min_args -= 1
670 671 self.bootstrap_config(args[0])
671 672 self.update_parser()
672 673 return super(BasePasterCommand, self).run(args[1:])
673 674
674 675 def update_parser(self):
675 676 """
676 677 Abstract method. Allows for the class's parser to be updated
677 678 before the superclass's `run` method is called. Necessary to
678 679 allow options/arguments to be passed through to the underlying
679 680 celery command.
680 681 """
681 682 raise NotImplementedError("Abstract Method.")
682 683
683 684 def bootstrap_config(self, conf):
684 685 """
685 686 Loads the pylons configuration.
686 687 """
687 688 from pylons import config as pylonsconfig
688 689
689 690 self.path_to_ini_file = os.path.realpath(conf)
690 691 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
691 692 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
692 693
693 694
694 695 def check_git_version():
695 696 """
696 697 Checks what version of git is installed in system, and issues a warning
697 698 if it's too old for RhodeCode to properly work.
698 699 """
699 700 import subprocess
700 701 from distutils.version import StrictVersion
701 702 from rhodecode import BACKENDS
702 703
703 704 p = subprocess.Popen('git --version', shell=True,
704 705 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
705 706 stdout, stderr = p.communicate()
706 707 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
707 708 if len(ver.split('.')) > 3:
708 709 #StrictVersion needs to be only 3 element type
709 710 ver = '.'.join(ver.split('.')[:3])
710 711 try:
711 712 _ver = StrictVersion(ver)
712 713 except:
713 714 _ver = StrictVersion('0.0.0')
714 715 stderr = traceback.format_exc()
715 716
716 717 req_ver = '1.7.4'
717 718 to_old_git = False
718 719 if _ver < StrictVersion(req_ver):
719 720 to_old_git = True
720 721
721 722 if 'git' in BACKENDS:
722 723 log.debug('GIT version detected: %s' % stdout)
723 724 if stderr:
724 725 log.warning('Unable to detect git version org error was:%r' % stderr)
725 726 elif to_old_git:
726 727 log.warning('RhodeCode detected git version %s, which is too old '
727 728 'for the system to function properly. Make sure '
728 729 'its version is at least %s' % (ver, req_ver))
729 730 return _ver
730 731
731 732
732 733 @decorator.decorator
733 734 def jsonify(func, *args, **kwargs):
734 735 """Action decorator that formats output for JSON
735 736
736 737 Given a function that will return content, this decorator will turn
737 738 the result into JSON, with a content-type of 'application/json' and
738 739 output it.
739 740
740 741 """
741 742 from pylons.decorators.util import get_pylons
742 743 from rhodecode.lib.ext_json import json
743 744 pylons = get_pylons(args)
744 745 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
745 746 data = func(*args, **kwargs)
746 747 if isinstance(data, (list, tuple)):
747 748 msg = "JSON responses with Array envelopes are susceptible to " \
748 749 "cross-site data leak attacks, see " \
749 750 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
750 751 warnings.warn(msg, Warning, 2)
751 752 log.warning(msg)
752 753 log.debug("Returning JSON wrapped action output")
753 754 return json.dumps(data, encoding='utf-8') No newline at end of file
@@ -1,996 +1,997 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.base
4 4 ~~~~~~~~~~~~~~~~~
5 5
6 6 Base for all available scm backends
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12
13 13 from itertools import chain
14 14 from rhodecode.lib.vcs.utils import author_name, author_email
15 15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 17 from rhodecode.lib.vcs.conf import settings
18 18
19 19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 22 RepositoryError
23 23
24 24
25 25 class BaseRepository(object):
26 26 """
27 27 Base Repository for final backends
28 28
29 29 **Attributes**
30 30
31 31 ``DEFAULT_BRANCH_NAME``
32 32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33 33
34 34 ``scm``
35 35 alias of scm, i.e. *git* or *hg*
36 36
37 37 ``repo``
38 38 object from external api
39 39
40 40 ``revisions``
41 41 list of all available revisions' ids, in ascending order
42 42
43 43 ``changesets``
44 44 storage dict caching returned changesets
45 45
46 46 ``path``
47 47 absolute path to the repository
48 48
49 49 ``branches``
50 50 branches as list of changesets
51 51
52 52 ``tags``
53 53 tags as list of changesets
54 54 """
55 55 scm = None
56 56 DEFAULT_BRANCH_NAME = None
57 57 EMPTY_CHANGESET = '0' * 40
58 58
59 59 def __init__(self, repo_path, create=False, **kwargs):
60 60 """
61 61 Initializes repository. Raises RepositoryError if repository could
62 62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 63 exists and ``create`` is set to True.
64 64
65 65 :param repo_path: local path of the repository
66 66 :param create=False: if set to True, would try to craete repository.
67 67 :param src_url=None: if set, should be proper url from which repository
68 68 would be cloned; requires ``create`` parameter to be set to True -
69 69 raises RepositoryError if src_url is set and create evaluates to
70 70 False
71 71 """
72 72 raise NotImplementedError
73 73
74 74 def __str__(self):
75 75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76 76
77 77 def __repr__(self):
78 78 return self.__str__()
79 79
80 80 def __len__(self):
81 81 return self.count()
82 82
83 83 @LazyProperty
84 84 def alias(self):
85 85 for k, v in settings.BACKENDS.items():
86 86 if v.split('.')[-1] == str(self.__class__.__name__):
87 87 return k
88 88
89 89 @LazyProperty
90 90 def name(self):
91 91 raise NotImplementedError
92 92
93 93 @LazyProperty
94 94 def owner(self):
95 95 raise NotImplementedError
96 96
97 97 @LazyProperty
98 98 def description(self):
99 99 raise NotImplementedError
100 100
101 101 @LazyProperty
102 102 def size(self):
103 103 """
104 104 Returns combined size in bytes for all repository files
105 105 """
106 106
107 107 size = 0
108 108 try:
109 109 tip = self.get_changeset()
110 110 for topnode, dirs, files in tip.walk('/'):
111 111 for f in files:
112 112 size += tip.get_file_size(f.path)
113 113 for dir in dirs:
114 114 for f in files:
115 115 size += tip.get_file_size(f.path)
116 116
117 117 except RepositoryError, e:
118 118 pass
119 119 return size
120 120
121 121 def is_valid(self):
122 122 """
123 123 Validates repository.
124 124 """
125 125 raise NotImplementedError
126 126
127 127 def get_last_change(self):
128 128 self.get_changesets()
129 129
130 130 #==========================================================================
131 131 # CHANGESETS
132 132 #==========================================================================
133 133
134 134 def get_changeset(self, revision=None):
135 135 """
136 136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 137 recent changeset is returned.
138 138
139 139 :raises ``EmptyRepositoryError``: if there are no revisions
140 140 """
141 141 raise NotImplementedError
142 142
143 143 def __iter__(self):
144 144 """
145 145 Allows Repository objects to be iterated.
146 146
147 147 *Requires* implementation of ``__getitem__`` method.
148 148 """
149 149 for revision in self.revisions:
150 150 yield self.get_changeset(revision)
151 151
152 152 def get_changesets(self, start=None, end=None, start_date=None,
153 153 end_date=None, branch_name=None, reverse=False):
154 154 """
155 155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 156 not inclusive This should behave just like a list, ie. end is not
157 157 inclusive
158 158
159 159 :param start: None or str
160 160 :param end: None or str
161 161 :param start_date:
162 162 :param end_date:
163 163 :param branch_name:
164 164 :param reversed:
165 165 """
166 166 raise NotImplementedError
167 167
168 168 def __getslice__(self, i, j):
169 169 """
170 170 Returns a iterator of sliced repository
171 171 """
172 172 for rev in self.revisions[i:j]:
173 173 yield self.get_changeset(rev)
174 174
175 175 def __getitem__(self, key):
176 176 return self.get_changeset(key)
177 177
178 178 def count(self):
179 179 return len(self.revisions)
180 180
181 181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 182 """
183 183 Creates and returns a tag for the given ``revision``.
184 184
185 185 :param name: name for new tag
186 186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 187 :param revision: changeset id for which new tag would be created
188 188 :param message: message of the tag's commit
189 189 :param date: date of tag's commit
190 190
191 191 :raises TagAlreadyExistError: if tag with same name already exists
192 192 """
193 193 raise NotImplementedError
194 194
195 195 def remove_tag(self, name, user, message=None, date=None):
196 196 """
197 197 Removes tag with the given ``name``.
198 198
199 199 :param name: name of the tag to be removed
200 200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 201 :param message: message of the tag's removal commit
202 202 :param date: date of tag's removal commit
203 203
204 204 :raises TagDoesNotExistError: if tag with given name does not exists
205 205 """
206 206 raise NotImplementedError
207 207
208 208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 209 context=3):
210 210 """
211 211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 212 ``rev2`` since ``rev1``.
213 213
214 214 :param rev1: Entry point from which diff is shown. Can be
215 215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 216 the changes since empty state of the repository until ``rev2``
217 217 :param rev2: Until which revision changes should be shown.
218 218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 219 changes. Defaults to ``False``.
220 220 :param context: How many lines before/after changed lines should be
221 221 shown. Defaults to ``3``.
222 222 """
223 223 raise NotImplementedError
224 224
225 225 # ========== #
226 226 # COMMIT API #
227 227 # ========== #
228 228
229 229 @LazyProperty
230 230 def in_memory_changeset(self):
231 231 """
232 232 Returns ``InMemoryChangeset`` object for this repository.
233 233 """
234 234 raise NotImplementedError
235 235
236 236 def add(self, filenode, **kwargs):
237 237 """
238 238 Commit api function that will add given ``FileNode`` into this
239 239 repository.
240 240
241 241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 242 already in repository
243 243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 244 *added*
245 245 """
246 246 raise NotImplementedError
247 247
248 248 def remove(self, filenode, **kwargs):
249 249 """
250 250 Commit api function that will remove given ``FileNode`` into this
251 251 repository.
252 252
253 253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 255 """
256 256 raise NotImplementedError
257 257
258 258 def commit(self, message, **kwargs):
259 259 """
260 260 Persists current changes made on this repository and returns newly
261 261 created changeset.
262 262
263 263 :raises ``NothingChangedError``: if no changes has been made
264 264 """
265 265 raise NotImplementedError
266 266
267 267 def get_state(self):
268 268 """
269 269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 270 containing ``FileNode`` objects.
271 271 """
272 272 raise NotImplementedError
273 273
274 274 def get_config_value(self, section, name, config_file=None):
275 275 """
276 276 Returns configuration value for a given [``section``] and ``name``.
277 277
278 278 :param section: Section we want to retrieve value from
279 279 :param name: Name of configuration we want to retrieve
280 280 :param config_file: A path to file which should be used to retrieve
281 281 configuration from (might also be a list of file paths)
282 282 """
283 283 raise NotImplementedError
284 284
285 285 def get_user_name(self, config_file=None):
286 286 """
287 287 Returns user's name from global configuration file.
288 288
289 289 :param config_file: A path to file which should be used to retrieve
290 290 configuration from (might also be a list of file paths)
291 291 """
292 292 raise NotImplementedError
293 293
294 294 def get_user_email(self, config_file=None):
295 295 """
296 296 Returns user's email from global configuration file.
297 297
298 298 :param config_file: A path to file which should be used to retrieve
299 299 configuration from (might also be a list of file paths)
300 300 """
301 301 raise NotImplementedError
302 302
303 303 # =========== #
304 304 # WORKDIR API #
305 305 # =========== #
306 306
307 307 @LazyProperty
308 308 def workdir(self):
309 309 """
310 310 Returns ``Workdir`` instance for this repository.
311 311 """
312 312 raise NotImplementedError
313 313
314 314
315 315 class BaseChangeset(object):
316 316 """
317 317 Each backend should implement it's changeset representation.
318 318
319 319 **Attributes**
320 320
321 321 ``repository``
322 322 repository object within which changeset exists
323 323
324 324 ``id``
325 325 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
326 326
327 327 ``raw_id``
328 328 raw changeset representation (i.e. full 40 length sha for git
329 329 backend)
330 330
331 331 ``short_id``
332 332 shortened (if apply) version of ``raw_id``; it would be simple
333 333 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
334 334 as ``raw_id`` for subversion
335 335
336 336 ``revision``
337 337 revision number as integer
338 338
339 339 ``files``
340 340 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
341 341
342 342 ``dirs``
343 343 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
344 344
345 345 ``nodes``
346 346 combined list of ``Node`` objects
347 347
348 348 ``author``
349 349 author of the changeset, as unicode
350 350
351 351 ``message``
352 352 message of the changeset, as unicode
353 353
354 354 ``parents``
355 355 list of parent changesets
356 356
357 357 ``last``
358 358 ``True`` if this is last changeset in repository, ``False``
359 359 otherwise; trying to access this attribute while there is no
360 360 changesets would raise ``EmptyRepositoryError``
361 361 """
362 362 def __str__(self):
363 363 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
364 364 self.short_id)
365 365
366 366 def __repr__(self):
367 367 return self.__str__()
368 368
369 369 def __unicode__(self):
370 370 return u'%s:%s' % (self.revision, self.short_id)
371 371
372 372 def __eq__(self, other):
373 373 return self.raw_id == other.raw_id
374 374
375 375 def __json__(self):
376 376 return dict(
377 377 short_id=self.short_id,
378 378 raw_id=self.raw_id,
379 revision=self.revision,
379 380 message=self.message,
380 381 date=self.date,
381 382 author=self.author,
382 383 )
383 384
384 385 @LazyProperty
385 386 def last(self):
386 387 if self.repository is None:
387 388 raise ChangesetError("Cannot check if it's most recent revision")
388 389 return self.raw_id == self.repository.revisions[-1]
389 390
390 391 @LazyProperty
391 392 def parents(self):
392 393 """
393 394 Returns list of parents changesets.
394 395 """
395 396 raise NotImplementedError
396 397
397 398 @LazyProperty
398 399 def children(self):
399 400 """
400 401 Returns list of children changesets.
401 402 """
402 403 raise NotImplementedError
403 404
404 405 @LazyProperty
405 406 def id(self):
406 407 """
407 408 Returns string identifying this changeset.
408 409 """
409 410 raise NotImplementedError
410 411
411 412 @LazyProperty
412 413 def raw_id(self):
413 414 """
414 415 Returns raw string identifying this changeset.
415 416 """
416 417 raise NotImplementedError
417 418
418 419 @LazyProperty
419 420 def short_id(self):
420 421 """
421 422 Returns shortened version of ``raw_id`` attribute, as string,
422 423 identifying this changeset, useful for web representation.
423 424 """
424 425 raise NotImplementedError
425 426
426 427 @LazyProperty
427 428 def revision(self):
428 429 """
429 430 Returns integer identifying this changeset.
430 431
431 432 """
432 433 raise NotImplementedError
433 434
434 435 @LazyProperty
435 436 def commiter(self):
436 437 """
437 438 Returns Commiter for given commit
438 439 """
439 440
440 441 raise NotImplementedError
441 442
442 443 @LazyProperty
443 444 def commiter_name(self):
444 445 """
445 446 Returns Author name for given commit
446 447 """
447 448
448 449 return author_name(self.commiter)
449 450
450 451 @LazyProperty
451 452 def commiter_email(self):
452 453 """
453 454 Returns Author email address for given commit
454 455 """
455 456
456 457 return author_email(self.commiter)
457 458
458 459 @LazyProperty
459 460 def author(self):
460 461 """
461 462 Returns Author for given commit
462 463 """
463 464
464 465 raise NotImplementedError
465 466
466 467 @LazyProperty
467 468 def author_name(self):
468 469 """
469 470 Returns Author name for given commit
470 471 """
471 472
472 473 return author_name(self.author)
473 474
474 475 @LazyProperty
475 476 def author_email(self):
476 477 """
477 478 Returns Author email address for given commit
478 479 """
479 480
480 481 return author_email(self.author)
481 482
482 483 def get_file_mode(self, path):
483 484 """
484 485 Returns stat mode of the file at the given ``path``.
485 486 """
486 487 raise NotImplementedError
487 488
488 489 def get_file_content(self, path):
489 490 """
490 491 Returns content of the file at the given ``path``.
491 492 """
492 493 raise NotImplementedError
493 494
494 495 def get_file_size(self, path):
495 496 """
496 497 Returns size of the file at the given ``path``.
497 498 """
498 499 raise NotImplementedError
499 500
500 501 def get_file_changeset(self, path):
501 502 """
502 503 Returns last commit of the file at the given ``path``.
503 504 """
504 505 raise NotImplementedError
505 506
506 507 def get_file_history(self, path):
507 508 """
508 509 Returns history of file as reversed list of ``Changeset`` objects for
509 510 which file at given ``path`` has been modified.
510 511 """
511 512 raise NotImplementedError
512 513
513 514 def get_nodes(self, path):
514 515 """
515 516 Returns combined ``DirNode`` and ``FileNode`` objects list representing
516 517 state of changeset at the given ``path``.
517 518
518 519 :raises ``ChangesetError``: if node at the given ``path`` is not
519 520 instance of ``DirNode``
520 521 """
521 522 raise NotImplementedError
522 523
523 524 def get_node(self, path):
524 525 """
525 526 Returns ``Node`` object from the given ``path``.
526 527
527 528 :raises ``NodeDoesNotExistError``: if there is no node at the given
528 529 ``path``
529 530 """
530 531 raise NotImplementedError
531 532
532 533 def fill_archive(self, stream=None, kind='tgz', prefix=None):
533 534 """
534 535 Fills up given stream.
535 536
536 537 :param stream: file like object.
537 538 :param kind: one of following: ``zip``, ``tar``, ``tgz``
538 539 or ``tbz2``. Default: ``tgz``.
539 540 :param prefix: name of root directory in archive.
540 541 Default is repository name and changeset's raw_id joined with dash.
541 542
542 543 repo-tip.<kind>
543 544 """
544 545
545 546 raise NotImplementedError
546 547
547 548 def get_chunked_archive(self, **kwargs):
548 549 """
549 550 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
550 551
551 552 :param chunk_size: extra parameter which controls size of returned
552 553 chunks. Default:8k.
553 554 """
554 555
555 556 chunk_size = kwargs.pop('chunk_size', 8192)
556 557 stream = kwargs.get('stream')
557 558 self.fill_archive(**kwargs)
558 559 while True:
559 560 data = stream.read(chunk_size)
560 561 if not data:
561 562 break
562 563 yield data
563 564
564 565 @LazyProperty
565 566 def root(self):
566 567 """
567 568 Returns ``RootNode`` object for this changeset.
568 569 """
569 570 return self.get_node('')
570 571
571 572 def next(self, branch=None):
572 573 """
573 574 Returns next changeset from current, if branch is gives it will return
574 575 next changeset belonging to this branch
575 576
576 577 :param branch: show changesets within the given named branch
577 578 """
578 579 raise NotImplementedError
579 580
580 581 def prev(self, branch=None):
581 582 """
582 583 Returns previous changeset from current, if branch is gives it will
583 584 return previous changeset belonging to this branch
584 585
585 586 :param branch: show changesets within the given named branch
586 587 """
587 588 raise NotImplementedError
588 589
589 590 @LazyProperty
590 591 def added(self):
591 592 """
592 593 Returns list of added ``FileNode`` objects.
593 594 """
594 595 raise NotImplementedError
595 596
596 597 @LazyProperty
597 598 def changed(self):
598 599 """
599 600 Returns list of modified ``FileNode`` objects.
600 601 """
601 602 raise NotImplementedError
602 603
603 604 @LazyProperty
604 605 def removed(self):
605 606 """
606 607 Returns list of removed ``FileNode`` objects.
607 608 """
608 609 raise NotImplementedError
609 610
610 611 @LazyProperty
611 612 def size(self):
612 613 """
613 614 Returns total number of bytes from contents of all filenodes.
614 615 """
615 616 return sum((node.size for node in self.get_filenodes_generator()))
616 617
617 618 def walk(self, topurl=''):
618 619 """
619 620 Similar to os.walk method. Insted of filesystem it walks through
620 621 changeset starting at given ``topurl``. Returns generator of tuples
621 622 (topnode, dirnodes, filenodes).
622 623 """
623 624 topnode = self.get_node(topurl)
624 625 yield (topnode, topnode.dirs, topnode.files)
625 626 for dirnode in topnode.dirs:
626 627 for tup in self.walk(dirnode.path):
627 628 yield tup
628 629
629 630 def get_filenodes_generator(self):
630 631 """
631 632 Returns generator that yields *all* file nodes.
632 633 """
633 634 for topnode, dirs, files in self.walk():
634 635 for node in files:
635 636 yield node
636 637
637 638 def as_dict(self):
638 639 """
639 640 Returns dictionary with changeset's attributes and their values.
640 641 """
641 642 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
642 643 'revision', 'date', 'message'])
643 644 data['author'] = {'name': self.author_name, 'email': self.author_email}
644 645 data['added'] = [node.path for node in self.added]
645 646 data['changed'] = [node.path for node in self.changed]
646 647 data['removed'] = [node.path for node in self.removed]
647 648 return data
648 649
649 650
650 651 class BaseWorkdir(object):
651 652 """
652 653 Working directory representation of single repository.
653 654
654 655 :attribute: repository: repository object of working directory
655 656 """
656 657
657 658 def __init__(self, repository):
658 659 self.repository = repository
659 660
660 661 def get_branch(self):
661 662 """
662 663 Returns name of current branch.
663 664 """
664 665 raise NotImplementedError
665 666
666 667 def get_changeset(self):
667 668 """
668 669 Returns current changeset.
669 670 """
670 671 raise NotImplementedError
671 672
672 673 def get_added(self):
673 674 """
674 675 Returns list of ``FileNode`` objects marked as *new* in working
675 676 directory.
676 677 """
677 678 raise NotImplementedError
678 679
679 680 def get_changed(self):
680 681 """
681 682 Returns list of ``FileNode`` objects *changed* in working directory.
682 683 """
683 684 raise NotImplementedError
684 685
685 686 def get_removed(self):
686 687 """
687 688 Returns list of ``RemovedFileNode`` objects marked as *removed* in
688 689 working directory.
689 690 """
690 691 raise NotImplementedError
691 692
692 693 def get_untracked(self):
693 694 """
694 695 Returns list of ``FileNode`` objects which are present within working
695 696 directory however are not tracked by repository.
696 697 """
697 698 raise NotImplementedError
698 699
699 700 def get_status(self):
700 701 """
701 702 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
702 703 lists.
703 704 """
704 705 raise NotImplementedError
705 706
706 707 def commit(self, message, **kwargs):
707 708 """
708 709 Commits local (from working directory) changes and returns newly
709 710 created
710 711 ``Changeset``. Updates repository's ``revisions`` list.
711 712
712 713 :raises ``CommitError``: if any error occurs while committing
713 714 """
714 715 raise NotImplementedError
715 716
716 717 def update(self, revision=None):
717 718 """
718 719 Fetches content of the given revision and populates it within working
719 720 directory.
720 721 """
721 722 raise NotImplementedError
722 723
723 724 def checkout_branch(self, branch=None):
724 725 """
725 726 Checks out ``branch`` or the backend's default branch.
726 727
727 728 Raises ``BranchDoesNotExistError`` if the branch does not exist.
728 729 """
729 730 raise NotImplementedError
730 731
731 732
732 733 class BaseInMemoryChangeset(object):
733 734 """
734 735 Represents differences between repository's state (most recent head) and
735 736 changes made *in place*.
736 737
737 738 **Attributes**
738 739
739 740 ``repository``
740 741 repository object for this in-memory-changeset
741 742
742 743 ``added``
743 744 list of ``FileNode`` objects marked as *added*
744 745
745 746 ``changed``
746 747 list of ``FileNode`` objects marked as *changed*
747 748
748 749 ``removed``
749 750 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
750 751 *removed*
751 752
752 753 ``parents``
753 754 list of ``Changeset`` representing parents of in-memory changeset.
754 755 Should always be 2-element sequence.
755 756
756 757 """
757 758
758 759 def __init__(self, repository):
759 760 self.repository = repository
760 761 self.added = []
761 762 self.changed = []
762 763 self.removed = []
763 764 self.parents = []
764 765
765 766 def add(self, *filenodes):
766 767 """
767 768 Marks given ``FileNode`` objects as *to be committed*.
768 769
769 770 :raises ``NodeAlreadyExistsError``: if node with same path exists at
770 771 latest changeset
771 772 :raises ``NodeAlreadyAddedError``: if node with same path is already
772 773 marked as *added*
773 774 """
774 775 # Check if not already marked as *added* first
775 776 for node in filenodes:
776 777 if node.path in (n.path for n in self.added):
777 778 raise NodeAlreadyAddedError("Such FileNode %s is already "
778 779 "marked for addition" % node.path)
779 780 for node in filenodes:
780 781 self.added.append(node)
781 782
782 783 def change(self, *filenodes):
783 784 """
784 785 Marks given ``FileNode`` objects to be *changed* in next commit.
785 786
786 787 :raises ``EmptyRepositoryError``: if there are no changesets yet
787 788 :raises ``NodeAlreadyExistsError``: if node with same path is already
788 789 marked to be *changed*
789 790 :raises ``NodeAlreadyRemovedError``: if node with same path is already
790 791 marked to be *removed*
791 792 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
792 793 changeset
793 794 :raises ``NodeNotChangedError``: if node hasn't really be changed
794 795 """
795 796 for node in filenodes:
796 797 if node.path in (n.path for n in self.removed):
797 798 raise NodeAlreadyRemovedError("Node at %s is already marked "
798 799 "as removed" % node.path)
799 800 try:
800 801 self.repository.get_changeset()
801 802 except EmptyRepositoryError:
802 803 raise EmptyRepositoryError("Nothing to change - try to *add* new "
803 804 "nodes rather than changing them")
804 805 for node in filenodes:
805 806 if node.path in (n.path for n in self.changed):
806 807 raise NodeAlreadyChangedError("Node at '%s' is already "
807 808 "marked as changed" % node.path)
808 809 self.changed.append(node)
809 810
810 811 def remove(self, *filenodes):
811 812 """
812 813 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
813 814 *removed* in next commit.
814 815
815 816 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
816 817 be *removed*
817 818 :raises ``NodeAlreadyChangedError``: if node has been already marked to
818 819 be *changed*
819 820 """
820 821 for node in filenodes:
821 822 if node.path in (n.path for n in self.removed):
822 823 raise NodeAlreadyRemovedError("Node is already marked to "
823 824 "for removal at %s" % node.path)
824 825 if node.path in (n.path for n in self.changed):
825 826 raise NodeAlreadyChangedError("Node is already marked to "
826 827 "be changed at %s" % node.path)
827 828 # We only mark node as *removed* - real removal is done by
828 829 # commit method
829 830 self.removed.append(node)
830 831
831 832 def reset(self):
832 833 """
833 834 Resets this instance to initial state (cleans ``added``, ``changed``
834 835 and ``removed`` lists).
835 836 """
836 837 self.added = []
837 838 self.changed = []
838 839 self.removed = []
839 840 self.parents = []
840 841
841 842 def get_ipaths(self):
842 843 """
843 844 Returns generator of paths from nodes marked as added, changed or
844 845 removed.
845 846 """
846 847 for node in chain(self.added, self.changed, self.removed):
847 848 yield node.path
848 849
849 850 def get_paths(self):
850 851 """
851 852 Returns list of paths from nodes marked as added, changed or removed.
852 853 """
853 854 return list(self.get_ipaths())
854 855
855 856 def check_integrity(self, parents=None):
856 857 """
857 858 Checks in-memory changeset's integrity. Also, sets parents if not
858 859 already set.
859 860
860 861 :raises CommitError: if any error occurs (i.e.
861 862 ``NodeDoesNotExistError``).
862 863 """
863 864 if not self.parents:
864 865 parents = parents or []
865 866 if len(parents) == 0:
866 867 try:
867 868 parents = [self.repository.get_changeset(), None]
868 869 except EmptyRepositoryError:
869 870 parents = [None, None]
870 871 elif len(parents) == 1:
871 872 parents += [None]
872 873 self.parents = parents
873 874
874 875 # Local parents, only if not None
875 876 parents = [p for p in self.parents if p]
876 877
877 878 # Check nodes marked as added
878 879 for p in parents:
879 880 for node in self.added:
880 881 try:
881 882 p.get_node(node.path)
882 883 except NodeDoesNotExistError:
883 884 pass
884 885 else:
885 886 raise NodeAlreadyExistsError("Node at %s already exists "
886 887 "at %s" % (node.path, p))
887 888
888 889 # Check nodes marked as changed
889 890 missing = set(self.changed)
890 891 not_changed = set(self.changed)
891 892 if self.changed and not parents:
892 893 raise NodeDoesNotExistError(str(self.changed[0].path))
893 894 for p in parents:
894 895 for node in self.changed:
895 896 try:
896 897 old = p.get_node(node.path)
897 898 missing.remove(node)
898 899 if old.content != node.content:
899 900 not_changed.remove(node)
900 901 except NodeDoesNotExistError:
901 902 pass
902 903 if self.changed and missing:
903 904 raise NodeDoesNotExistError("Node at %s is missing "
904 905 "(parents: %s)" % (node.path, parents))
905 906
906 907 if self.changed and not_changed:
907 908 raise NodeNotChangedError("Node at %s wasn't actually changed "
908 909 "since parents' changesets: %s" % (not_changed.pop().path,
909 910 parents)
910 911 )
911 912
912 913 # Check nodes marked as removed
913 914 if self.removed and not parents:
914 915 raise NodeDoesNotExistError("Cannot remove node at %s as there "
915 916 "were no parents specified" % self.removed[0].path)
916 917 really_removed = set()
917 918 for p in parents:
918 919 for node in self.removed:
919 920 try:
920 921 p.get_node(node.path)
921 922 really_removed.add(node)
922 923 except ChangesetError:
923 924 pass
924 925 not_removed = set(self.removed) - really_removed
925 926 if not_removed:
926 927 raise NodeDoesNotExistError("Cannot remove node at %s from "
927 928 "following parents: %s" % (not_removed[0], parents))
928 929
929 930 def commit(self, message, author, parents=None, branch=None, date=None,
930 931 **kwargs):
931 932 """
932 933 Performs in-memory commit (doesn't check workdir in any way) and
933 934 returns newly created ``Changeset``. Updates repository's
934 935 ``revisions``.
935 936
936 937 .. note::
937 938 While overriding this method each backend's should call
938 939 ``self.check_integrity(parents)`` in the first place.
939 940
940 941 :param message: message of the commit
941 942 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
942 943 :param parents: single parent or sequence of parents from which commit
943 944 would be derieved
944 945 :param date: ``datetime.datetime`` instance. Defaults to
945 946 ``datetime.datetime.now()``.
946 947 :param branch: branch name, as string. If none given, default backend's
947 948 branch would be used.
948 949
949 950 :raises ``CommitError``: if any error occurs while committing
950 951 """
951 952 raise NotImplementedError
952 953
953 954
954 955 class EmptyChangeset(BaseChangeset):
955 956 """
956 957 An dummy empty changeset. It's possible to pass hash when creating
957 958 an EmptyChangeset
958 959 """
959 960
960 961 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
961 962 alias=None, revision=-1, message='', author='', date=''):
962 963 self._empty_cs = cs
963 964 self.revision = revision
964 965 self.message = message
965 966 self.author = author
966 967 self.date = date
967 968 self.repository = repo
968 969 self.requested_revision = requested_revision
969 970 self.alias = alias
970 971
971 972 @LazyProperty
972 973 def raw_id(self):
973 974 """
974 975 Returns raw string identifying this changeset, useful for web
975 976 representation.
976 977 """
977 978
978 979 return self._empty_cs
979 980
980 981 @LazyProperty
981 982 def branch(self):
982 983 from rhodecode.lib.vcs.backends import get_backend
983 984 return get_backend(self.alias).DEFAULT_BRANCH_NAME
984 985
985 986 @LazyProperty
986 987 def short_id(self):
987 988 return self.raw_id[:12]
988 989
989 990 def get_file_changeset(self, path):
990 991 return self
991 992
992 993 def get_file_content(self, path):
993 994 return u''
994 995
995 996 def get_file_size(self, path):
996 997 return 0
@@ -1,1872 +1,1908 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 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47
48 48 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
49 49 safe_unicode, remove_suffix, remove_prefix
50 50 from rhodecode.lib.compat import json
51 51 from rhodecode.lib.caching_query import FromCache
52 52
53 53 from rhodecode.model.meta import Base, Session
54 54
55 55 URL_SEP = '/'
56 56 log = logging.getLogger(__name__)
57 57
58 58 #==============================================================================
59 59 # BASE CLASSES
60 60 #==============================================================================
61 61
62 62 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
63 63
64 64
65 65 class BaseModel(object):
66 66 """
67 67 Base Model for all classess
68 68 """
69 69
70 70 @classmethod
71 71 def _get_keys(cls):
72 72 """return column names for this model """
73 73 return class_mapper(cls).c.keys()
74 74
75 75 def get_dict(self):
76 76 """
77 77 return dict with keys and values corresponding
78 78 to this model data """
79 79
80 80 d = {}
81 81 for k in self._get_keys():
82 82 d[k] = getattr(self, k)
83 83
84 84 # also use __json__() if present to get additional fields
85 85 _json_attr = getattr(self, '__json__', None)
86 86 if _json_attr:
87 87 # update with attributes from __json__
88 88 if callable(_json_attr):
89 89 _json_attr = _json_attr()
90 90 for k, val in _json_attr.iteritems():
91 91 d[k] = val
92 92 return d
93 93
94 94 def get_appstruct(self):
95 95 """return list with keys and values tupples corresponding
96 96 to this model data """
97 97
98 98 l = []
99 99 for k in self._get_keys():
100 100 l.append((k, getattr(self, k),))
101 101 return l
102 102
103 103 def populate_obj(self, populate_dict):
104 104 """populate model with data from given populate_dict"""
105 105
106 106 for k in self._get_keys():
107 107 if k in populate_dict:
108 108 setattr(self, k, populate_dict[k])
109 109
110 110 @classmethod
111 111 def query(cls):
112 112 return Session().query(cls)
113 113
114 114 @classmethod
115 115 def get(cls, id_):
116 116 if id_:
117 117 return cls.query().get(id_)
118 118
119 119 @classmethod
120 120 def get_or_404(cls, id_):
121 121 try:
122 122 id_ = int(id_)
123 123 except (TypeError, ValueError):
124 124 raise HTTPNotFound
125 125
126 126 res = cls.query().get(id_)
127 127 if not res:
128 128 raise HTTPNotFound
129 129 return res
130 130
131 131 @classmethod
132 132 def getAll(cls):
133 133 return cls.query().all()
134 134
135 135 @classmethod
136 136 def delete(cls, id_):
137 137 obj = cls.query().get(id_)
138 138 Session().delete(obj)
139 139
140 140 def __repr__(self):
141 141 if hasattr(self, '__unicode__'):
142 142 # python repr needs to return str
143 143 return safe_str(self.__unicode__())
144 144 return '<DB:%s>' % (self.__class__.__name__)
145 145
146 146
147 147 class RhodeCodeSetting(Base, BaseModel):
148 148 __tablename__ = 'rhodecode_settings'
149 149 __table_args__ = (
150 150 UniqueConstraint('app_settings_name'),
151 151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 152 'mysql_charset': 'utf8'}
153 153 )
154 154 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 155 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 156 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 157
158 158 def __init__(self, k='', v=''):
159 159 self.app_settings_name = k
160 160 self.app_settings_value = v
161 161
162 162 @validates('_app_settings_value')
163 163 def validate_settings_value(self, key, val):
164 164 assert type(val) == unicode
165 165 return val
166 166
167 167 @hybrid_property
168 168 def app_settings_value(self):
169 169 v = self._app_settings_value
170 170 if self.app_settings_name in ["ldap_active",
171 171 "default_repo_enable_statistics",
172 172 "default_repo_enable_locking",
173 173 "default_repo_private",
174 174 "default_repo_enable_downloads"]:
175 175 v = str2bool(v)
176 176 return v
177 177
178 178 @app_settings_value.setter
179 179 def app_settings_value(self, val):
180 180 """
181 181 Setter that will always make sure we use unicode in app_settings_value
182 182
183 183 :param val:
184 184 """
185 185 self._app_settings_value = safe_unicode(val)
186 186
187 187 def __unicode__(self):
188 188 return u"<%s('%s:%s')>" % (
189 189 self.__class__.__name__,
190 190 self.app_settings_name, self.app_settings_value
191 191 )
192 192
193 193 @classmethod
194 194 def get_by_name(cls, key):
195 195 return cls.query()\
196 196 .filter(cls.app_settings_name == key).scalar()
197 197
198 198 @classmethod
199 199 def get_by_name_or_create(cls, key):
200 200 res = cls.get_by_name(key)
201 201 if not res:
202 202 res = cls(key)
203 203 return res
204 204
205 205 @classmethod
206 206 def get_app_settings(cls, cache=False):
207 207
208 208 ret = cls.query()
209 209
210 210 if cache:
211 211 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
212 212
213 213 if not ret:
214 214 raise Exception('Could not get application settings !')
215 215 settings = {}
216 216 for each in ret:
217 217 settings['rhodecode_' + each.app_settings_name] = \
218 218 each.app_settings_value
219 219
220 220 return settings
221 221
222 222 @classmethod
223 223 def get_ldap_settings(cls, cache=False):
224 224 ret = cls.query()\
225 225 .filter(cls.app_settings_name.startswith('ldap_')).all()
226 226 fd = {}
227 227 for row in ret:
228 228 fd.update({row.app_settings_name: row.app_settings_value})
229 229
230 230 return fd
231 231
232 232 @classmethod
233 233 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
234 234 ret = cls.query()\
235 235 .filter(cls.app_settings_name.startswith('default_')).all()
236 236 fd = {}
237 237 for row in ret:
238 238 key = row.app_settings_name
239 239 if strip_prefix:
240 240 key = remove_prefix(key, prefix='default_')
241 241 fd.update({key: row.app_settings_value})
242 242
243 243 return fd
244 244
245 245
246 246 class RhodeCodeUi(Base, BaseModel):
247 247 __tablename__ = 'rhodecode_ui'
248 248 __table_args__ = (
249 249 UniqueConstraint('ui_key'),
250 250 {'extend_existing': True, 'mysql_engine': 'InnoDB',
251 251 'mysql_charset': 'utf8'}
252 252 )
253 253
254 254 HOOK_UPDATE = 'changegroup.update'
255 255 HOOK_REPO_SIZE = 'changegroup.repo_size'
256 256 HOOK_PUSH = 'changegroup.push_logger'
257 257 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
258 258 HOOK_PULL = 'outgoing.pull_logger'
259 259 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
260 260
261 261 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
262 262 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
263 263 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
266 266
267 267 @classmethod
268 268 def get_by_key(cls, key):
269 269 return cls.query().filter(cls.ui_key == key).scalar()
270 270
271 271 @classmethod
272 272 def get_builtin_hooks(cls):
273 273 q = cls.query()
274 274 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
275 275 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
276 276 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
277 277 return q.all()
278 278
279 279 @classmethod
280 280 def get_custom_hooks(cls):
281 281 q = cls.query()
282 282 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
283 283 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
284 284 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
285 285 q = q.filter(cls.ui_section == 'hooks')
286 286 return q.all()
287 287
288 288 @classmethod
289 289 def get_repos_location(cls):
290 290 return cls.get_by_key('/').ui_value
291 291
292 292 @classmethod
293 293 def create_or_update_hook(cls, key, val):
294 294 new_ui = cls.get_by_key(key) or cls()
295 295 new_ui.ui_section = 'hooks'
296 296 new_ui.ui_active = True
297 297 new_ui.ui_key = key
298 298 new_ui.ui_value = val
299 299
300 300 Session().add(new_ui)
301 301
302 302 def __repr__(self):
303 303 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
304 304 self.ui_value)
305 305
306 306
307 307 class User(Base, BaseModel):
308 308 __tablename__ = 'users'
309 309 __table_args__ = (
310 310 UniqueConstraint('username'), UniqueConstraint('email'),
311 311 Index('u_username_idx', 'username'),
312 312 Index('u_email_idx', 'email'),
313 313 {'extend_existing': True, 'mysql_engine': 'InnoDB',
314 314 'mysql_charset': 'utf8'}
315 315 )
316 316 DEFAULT_USER = 'default'
317 317 DEFAULT_PERMISSIONS = [
318 318 'hg.register.manual_activate', 'hg.create.repository',
319 319 'hg.fork.repository', 'repository.read', 'group.read'
320 320 ]
321 321 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
322 322 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
323 323 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 324 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
325 325 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
326 326 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 327 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 328 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 329 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
330 330 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 331 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
333 333
334 334 user_log = relationship('UserLog')
335 335 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
336 336
337 337 repositories = relationship('Repository')
338 338 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
339 339 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
340 340
341 341 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
342 342 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
343 343
344 344 group_member = relationship('UsersGroupMember', cascade='all')
345 345
346 346 notifications = relationship('UserNotification', cascade='all')
347 347 # notifications assigned to this user
348 348 user_created_notifications = relationship('Notification', cascade='all')
349 349 # comments created by this user
350 350 user_comments = relationship('ChangesetComment', cascade='all')
351 351 #extra emails for this user
352 352 user_emails = relationship('UserEmailMap', cascade='all')
353 353
354 354 @hybrid_property
355 355 def email(self):
356 356 return self._email
357 357
358 358 @email.setter
359 359 def email(self, val):
360 360 self._email = val.lower() if val else None
361 361
362 362 @property
363 363 def firstname(self):
364 364 # alias for future
365 365 return self.name
366 366
367 367 @property
368 368 def emails(self):
369 369 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
370 370 return [self.email] + [x.email for x in other]
371 371
372 372 @property
373 373 def ip_addresses(self):
374 374 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
375 375 return [x.ip_addr for x in ret]
376 376
377 377 @property
378 378 def username_and_name(self):
379 379 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
380 380
381 381 @property
382 382 def full_name(self):
383 383 return '%s %s' % (self.firstname, self.lastname)
384 384
385 385 @property
386 386 def full_name_or_username(self):
387 387 return ('%s %s' % (self.firstname, self.lastname)
388 388 if (self.firstname and self.lastname) else self.username)
389 389
390 390 @property
391 391 def full_contact(self):
392 392 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
393 393
394 394 @property
395 395 def short_contact(self):
396 396 return '%s %s' % (self.firstname, self.lastname)
397 397
398 398 @property
399 399 def is_admin(self):
400 400 return self.admin
401 401
402 402 def __unicode__(self):
403 403 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
404 404 self.user_id, self.username)
405 405
406 406 @classmethod
407 407 def get_by_username(cls, username, case_insensitive=False, cache=False):
408 408 if case_insensitive:
409 409 q = cls.query().filter(cls.username.ilike(username))
410 410 else:
411 411 q = cls.query().filter(cls.username == username)
412 412
413 413 if cache:
414 414 q = q.options(FromCache(
415 415 "sql_cache_short",
416 416 "get_user_%s" % _hash_key(username)
417 417 )
418 418 )
419 419 return q.scalar()
420 420
421 421 @classmethod
422 422 def get_by_api_key(cls, api_key, cache=False):
423 423 q = cls.query().filter(cls.api_key == api_key)
424 424
425 425 if cache:
426 426 q = q.options(FromCache("sql_cache_short",
427 427 "get_api_key_%s" % api_key))
428 428 return q.scalar()
429 429
430 430 @classmethod
431 431 def get_by_email(cls, email, case_insensitive=False, cache=False):
432 432 if case_insensitive:
433 433 q = cls.query().filter(cls.email.ilike(email))
434 434 else:
435 435 q = cls.query().filter(cls.email == email)
436 436
437 437 if cache:
438 438 q = q.options(FromCache("sql_cache_short",
439 439 "get_email_key_%s" % email))
440 440
441 441 ret = q.scalar()
442 442 if ret is None:
443 443 q = UserEmailMap.query()
444 444 # try fetching in alternate email map
445 445 if case_insensitive:
446 446 q = q.filter(UserEmailMap.email.ilike(email))
447 447 else:
448 448 q = q.filter(UserEmailMap.email == email)
449 449 q = q.options(joinedload(UserEmailMap.user))
450 450 if cache:
451 451 q = q.options(FromCache("sql_cache_short",
452 452 "get_email_map_key_%s" % email))
453 453 ret = getattr(q.scalar(), 'user', None)
454 454
455 455 return ret
456 456
457 457 def update_lastlogin(self):
458 458 """Update user lastlogin"""
459 459 self.last_login = datetime.datetime.now()
460 460 Session().add(self)
461 461 log.debug('updated user %s lastlogin' % self.username)
462 462
463 463 def get_api_data(self):
464 464 """
465 465 Common function for generating user related data for API
466 466 """
467 467 user = self
468 468 data = dict(
469 469 user_id=user.user_id,
470 470 username=user.username,
471 471 firstname=user.name,
472 472 lastname=user.lastname,
473 473 email=user.email,
474 474 emails=user.emails,
475 475 api_key=user.api_key,
476 476 active=user.active,
477 477 admin=user.admin,
478 478 ldap_dn=user.ldap_dn,
479 479 last_login=user.last_login,
480 480 ip_addresses=user.ip_addresses
481 481 )
482 482 return data
483 483
484 484 def __json__(self):
485 485 data = dict(
486 486 full_name=self.full_name,
487 487 full_name_or_username=self.full_name_or_username,
488 488 short_contact=self.short_contact,
489 489 full_contact=self.full_contact
490 490 )
491 491 data.update(self.get_api_data())
492 492 return data
493 493
494 494
495 495 class UserEmailMap(Base, BaseModel):
496 496 __tablename__ = 'user_email_map'
497 497 __table_args__ = (
498 498 Index('uem_email_idx', 'email'),
499 499 UniqueConstraint('email'),
500 500 {'extend_existing': True, 'mysql_engine': 'InnoDB',
501 501 'mysql_charset': 'utf8'}
502 502 )
503 503 __mapper_args__ = {}
504 504
505 505 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
506 506 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
507 507 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
508 508 user = relationship('User', lazy='joined')
509 509
510 510 @validates('_email')
511 511 def validate_email(self, key, email):
512 512 # check if this email is not main one
513 513 main_email = Session().query(User).filter(User.email == email).scalar()
514 514 if main_email is not None:
515 515 raise AttributeError('email %s is present is user table' % email)
516 516 return email
517 517
518 518 @hybrid_property
519 519 def email(self):
520 520 return self._email
521 521
522 522 @email.setter
523 523 def email(self, val):
524 524 self._email = val.lower() if val else None
525 525
526 526
527 527 class UserIpMap(Base, BaseModel):
528 528 __tablename__ = 'user_ip_map'
529 529 __table_args__ = (
530 530 UniqueConstraint('user_id', 'ip_addr'),
531 531 {'extend_existing': True, 'mysql_engine': 'InnoDB',
532 532 'mysql_charset': 'utf8'}
533 533 )
534 534 __mapper_args__ = {}
535 535
536 536 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
537 537 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
538 538 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
539 539 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
540 540 user = relationship('User', lazy='joined')
541 541
542 542 @classmethod
543 543 def _get_ip_range(cls, ip_addr):
544 544 from rhodecode.lib import ipaddr
545 545 net = ipaddr.IPv4Network(ip_addr)
546 546 return [str(net.network), str(net.broadcast)]
547 547
548 548 def __json__(self):
549 549 return dict(
550 550 ip_addr=self.ip_addr,
551 551 ip_range=self._get_ip_range(self.ip_addr)
552 552 )
553 553
554 554
555 555 class UserLog(Base, BaseModel):
556 556 __tablename__ = 'user_logs'
557 557 __table_args__ = (
558 558 {'extend_existing': True, 'mysql_engine': 'InnoDB',
559 559 'mysql_charset': 'utf8'},
560 560 )
561 561 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
562 562 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
563 563 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
564 564 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
565 565 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
566 566 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
567 567 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
568 568 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
569 569
570 570 @property
571 571 def action_as_day(self):
572 572 return datetime.date(*self.action_date.timetuple()[:3])
573 573
574 574 user = relationship('User')
575 575 repository = relationship('Repository', cascade='')
576 576
577 577
578 578 class UsersGroup(Base, BaseModel):
579 579 __tablename__ = 'users_groups'
580 580 __table_args__ = (
581 581 {'extend_existing': True, 'mysql_engine': 'InnoDB',
582 582 'mysql_charset': 'utf8'},
583 583 )
584 584
585 585 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
586 586 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
587 587 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
588 588 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
589 589
590 590 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
591 591 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
592 592 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
593 593
594 594 def __unicode__(self):
595 595 return u'<userGroup(%s)>' % (self.users_group_name)
596 596
597 597 @classmethod
598 598 def get_by_group_name(cls, group_name, cache=False,
599 599 case_insensitive=False):
600 600 if case_insensitive:
601 601 q = cls.query().filter(cls.users_group_name.ilike(group_name))
602 602 else:
603 603 q = cls.query().filter(cls.users_group_name == group_name)
604 604 if cache:
605 605 q = q.options(FromCache(
606 606 "sql_cache_short",
607 607 "get_user_%s" % _hash_key(group_name)
608 608 )
609 609 )
610 610 return q.scalar()
611 611
612 612 @classmethod
613 613 def get(cls, users_group_id, cache=False):
614 614 users_group = cls.query()
615 615 if cache:
616 616 users_group = users_group.options(FromCache("sql_cache_short",
617 617 "get_users_group_%s" % users_group_id))
618 618 return users_group.get(users_group_id)
619 619
620 620 def get_api_data(self):
621 621 users_group = self
622 622
623 623 data = dict(
624 624 users_group_id=users_group.users_group_id,
625 625 group_name=users_group.users_group_name,
626 626 active=users_group.users_group_active,
627 627 )
628 628
629 629 return data
630 630
631 631
632 632 class UsersGroupMember(Base, BaseModel):
633 633 __tablename__ = 'users_groups_members'
634 634 __table_args__ = (
635 635 {'extend_existing': True, 'mysql_engine': 'InnoDB',
636 636 'mysql_charset': 'utf8'},
637 637 )
638 638
639 639 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
640 640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
641 641 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
642 642
643 643 user = relationship('User', lazy='joined')
644 644 users_group = relationship('UsersGroup')
645 645
646 646 def __init__(self, gr_id='', u_id=''):
647 647 self.users_group_id = gr_id
648 648 self.user_id = u_id
649 649
650 650
651 651 class Repository(Base, BaseModel):
652 652 __tablename__ = 'repositories'
653 653 __table_args__ = (
654 654 UniqueConstraint('repo_name'),
655 655 Index('r_repo_name_idx', 'repo_name'),
656 656 {'extend_existing': True, 'mysql_engine': 'InnoDB',
657 657 'mysql_charset': 'utf8'},
658 658 )
659 659
660 660 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
661 661 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
662 662 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
663 663 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
664 664 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
665 665 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
666 666 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
667 667 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
668 668 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
669 669 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
670 670 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
671 671 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
672 672 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
673 673 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
674 #changeset_cache = Column("changeset_cache", LargeBinary(), nullable=False) #JSON data
674 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
675 675
676 676 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
677 677 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
678 678
679 679 user = relationship('User')
680 680 fork = relationship('Repository', remote_side=repo_id)
681 681 group = relationship('RepoGroup')
682 682 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
683 683 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
684 684 stats = relationship('Statistics', cascade='all', uselist=False)
685 685
686 686 followers = relationship('UserFollowing',
687 687 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
688 688 cascade='all')
689 689
690 690 logs = relationship('UserLog')
691 691 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
692 692
693 693 pull_requests_org = relationship('PullRequest',
694 694 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
695 695 cascade="all, delete, delete-orphan")
696 696
697 697 pull_requests_other = relationship('PullRequest',
698 698 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
699 699 cascade="all, delete, delete-orphan")
700 700
701 701 def __unicode__(self):
702 702 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
703 703 self.repo_name)
704 704
705 705 @hybrid_property
706 706 def locked(self):
707 707 # always should return [user_id, timelocked]
708 708 if self._locked:
709 709 _lock_info = self._locked.split(':')
710 710 return int(_lock_info[0]), _lock_info[1]
711 711 return [None, None]
712 712
713 713 @locked.setter
714 714 def locked(self, val):
715 715 if val and isinstance(val, (list, tuple)):
716 716 self._locked = ':'.join(map(str, val))
717 717 else:
718 718 self._locked = None
719 719
720 @hybrid_property
721 def changeset_cache(self):
722 from rhodecode.lib.vcs.backends.base import EmptyChangeset
723 dummy = EmptyChangeset().__json__()
724 if not self._changeset_cache:
725 return dummy
726 try:
727 return json.loads(self._changeset_cache)
728 except TypeError:
729 return dummy
730
731 @changeset_cache.setter
732 def changeset_cache(self, val):
733 try:
734 self._changeset_cache = json.dumps(val)
735 except:
736 log.error(traceback.format_exc())
737
720 738 @classmethod
721 739 def url_sep(cls):
722 740 return URL_SEP
723 741
724 742 @classmethod
725 743 def get_by_repo_name(cls, repo_name):
726 744 q = Session().query(cls).filter(cls.repo_name == repo_name)
727 745 q = q.options(joinedload(Repository.fork))\
728 746 .options(joinedload(Repository.user))\
729 747 .options(joinedload(Repository.group))
730 748 return q.scalar()
731 749
732 750 @classmethod
733 751 def get_by_full_path(cls, repo_full_path):
734 752 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
735 753 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
736 754
737 755 @classmethod
738 756 def get_repo_forks(cls, repo_id):
739 757 return cls.query().filter(Repository.fork_id == repo_id)
740 758
741 759 @classmethod
742 760 def base_path(cls):
743 761 """
744 762 Returns base path when all repos are stored
745 763
746 764 :param cls:
747 765 """
748 766 q = Session().query(RhodeCodeUi)\
749 767 .filter(RhodeCodeUi.ui_key == cls.url_sep())
750 768 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
751 769 return q.one().ui_value
752 770
753 771 @property
754 772 def forks(self):
755 773 """
756 774 Return forks of this repo
757 775 """
758 776 return Repository.get_repo_forks(self.repo_id)
759 777
760 778 @property
761 779 def parent(self):
762 780 """
763 781 Returns fork parent
764 782 """
765 783 return self.fork
766 784
767 785 @property
768 786 def just_name(self):
769 787 return self.repo_name.split(Repository.url_sep())[-1]
770 788
771 789 @property
772 790 def groups_with_parents(self):
773 791 groups = []
774 792 if self.group is None:
775 793 return groups
776 794
777 795 cur_gr = self.group
778 796 groups.insert(0, cur_gr)
779 797 while 1:
780 798 gr = getattr(cur_gr, 'parent_group', None)
781 799 cur_gr = cur_gr.parent_group
782 800 if gr is None:
783 801 break
784 802 groups.insert(0, gr)
785 803
786 804 return groups
787 805
788 806 @property
789 807 def groups_and_repo(self):
790 808 return self.groups_with_parents, self.just_name
791 809
792 810 @LazyProperty
793 811 def repo_path(self):
794 812 """
795 813 Returns base full path for that repository means where it actually
796 814 exists on a filesystem
797 815 """
798 816 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
799 817 Repository.url_sep())
800 818 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
801 819 return q.one().ui_value
802 820
803 821 @property
804 822 def repo_full_path(self):
805 823 p = [self.repo_path]
806 824 # we need to split the name by / since this is how we store the
807 825 # names in the database, but that eventually needs to be converted
808 826 # into a valid system path
809 827 p += self.repo_name.split(Repository.url_sep())
810 828 return os.path.join(*p)
811 829
812 830 @property
813 831 def cache_keys(self):
814 832 """
815 833 Returns associated cache keys for that repo
816 834 """
817 835 return CacheInvalidation.query()\
818 836 .filter(CacheInvalidation.cache_args == self.repo_name)\
819 837 .order_by(CacheInvalidation.cache_key)\
820 838 .all()
821 839
822 840 def get_new_name(self, repo_name):
823 841 """
824 842 returns new full repository name based on assigned group and new new
825 843
826 844 :param group_name:
827 845 """
828 846 path_prefix = self.group.full_path_splitted if self.group else []
829 847 return Repository.url_sep().join(path_prefix + [repo_name])
830 848
831 849 @property
832 850 def _ui(self):
833 851 """
834 852 Creates an db based ui object for this repository
835 853 """
836 854 from rhodecode.lib.utils import make_ui
837 855 return make_ui('db', clear_session=False)
838 856
839 857 @classmethod
840 858 def inject_ui(cls, repo, extras={}):
841 859 from rhodecode.lib.vcs.backends.hg import MercurialRepository
842 860 from rhodecode.lib.vcs.backends.git import GitRepository
843 861 required = (MercurialRepository, GitRepository)
844 862 if not isinstance(repo, required):
845 863 raise Exception('repo must be instance of %s' % required)
846 864
847 865 # inject ui extra param to log this action via push logger
848 866 for k, v in extras.items():
849 867 repo._repo.ui.setconfig('rhodecode_extras', k, v)
850 868
851 869 @classmethod
852 870 def is_valid(cls, repo_name):
853 871 """
854 872 returns True if given repo name is a valid filesystem repository
855 873
856 874 :param cls:
857 875 :param repo_name:
858 876 """
859 877 from rhodecode.lib.utils import is_valid_repo
860 878
861 879 return is_valid_repo(repo_name, cls.base_path())
862 880
863 881 def get_api_data(self):
864 882 """
865 883 Common function for generating repo api data
866 884
867 885 """
868 886 repo = self
869 887 data = dict(
870 888 repo_id=repo.repo_id,
871 889 repo_name=repo.repo_name,
872 890 repo_type=repo.repo_type,
873 891 clone_uri=repo.clone_uri,
874 892 private=repo.private,
875 893 created_on=repo.created_on,
876 894 description=repo.description,
877 895 landing_rev=repo.landing_rev,
878 896 owner=repo.user.username,
879 897 fork_of=repo.fork.repo_name if repo.fork else None,
880 898 enable_statistics=repo.enable_statistics,
881 899 enable_locking=repo.enable_locking,
882 900 enable_downloads=repo.enable_downloads
883 901 )
884 902
885 903 return data
886 904
887 905 @classmethod
888 906 def lock(cls, repo, user_id):
889 907 repo.locked = [user_id, time.time()]
890 908 Session().add(repo)
891 909 Session().commit()
892 910
893 911 @classmethod
894 912 def unlock(cls, repo):
895 913 repo.locked = None
896 914 Session().add(repo)
897 915 Session().commit()
898 916
899 917 @property
900 918 def last_db_change(self):
901 919 return self.updated_on
902 920
903 921 #==========================================================================
904 922 # SCM PROPERTIES
905 923 #==========================================================================
906 924
907 925 def get_changeset(self, rev=None):
908 926 return get_changeset_safe(self.scm_instance, rev)
909 927
910 928 def get_landing_changeset(self):
911 929 """
912 930 Returns landing changeset, or if that doesn't exist returns the tip
913 931 """
914 932 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
915 933 return cs
916 934
917 def update_last_change(self, last_change=None):
918 if last_change is None:
919 last_change = datetime.datetime.now()
920 if self.updated_on is None or self.updated_on != last_change:
921 log.debug('updated repo %s with new date %s' % (self, last_change))
935 def update_changeset_cache(self, cs_cache=None):
936 """
937 Update cache of last changeset for repository, keys should be::
938
939 short_id
940 raw_id
941 revision
942 message
943 date
944 author
945
946 :param cs_cache:
947 """
948 from rhodecode.lib.vcs.backends.base import BaseChangeset
949 if cs_cache is None:
950 cs_cache = self.get_changeset()
951 if isinstance(cs_cache, BaseChangeset):
952 cs_cache = cs_cache.__json__()
953
954 if cs_cache != self.changeset_cache:
955 last_change = cs_cache.get('date') or self.last_change
956 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
922 957 self.updated_on = last_change
958 self.changeset_cache = cs_cache
923 959 Session().add(self)
924 960 Session().commit()
925 961
926 962 @property
927 963 def tip(self):
928 964 return self.get_changeset('tip')
929 965
930 966 @property
931 967 def author(self):
932 968 return self.tip.author
933 969
934 970 @property
935 971 def last_change(self):
936 972 return self.scm_instance.last_change
937 973
938 974 def get_comments(self, revisions=None):
939 975 """
940 976 Returns comments for this repository grouped by revisions
941 977
942 978 :param revisions: filter query by revisions only
943 979 """
944 980 cmts = ChangesetComment.query()\
945 981 .filter(ChangesetComment.repo == self)
946 982 if revisions:
947 983 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
948 984 grouped = defaultdict(list)
949 985 for cmt in cmts.all():
950 986 grouped[cmt.revision].append(cmt)
951 987 return grouped
952 988
953 989 def statuses(self, revisions=None):
954 990 """
955 991 Returns statuses for this repository
956 992
957 993 :param revisions: list of revisions to get statuses for
958 994 :type revisions: list
959 995 """
960 996
961 997 statuses = ChangesetStatus.query()\
962 998 .filter(ChangesetStatus.repo == self)\
963 999 .filter(ChangesetStatus.version == 0)
964 1000 if revisions:
965 1001 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
966 1002 grouped = {}
967 1003
968 1004 #maybe we have open new pullrequest without a status ?
969 1005 stat = ChangesetStatus.STATUS_UNDER_REVIEW
970 1006 status_lbl = ChangesetStatus.get_status_lbl(stat)
971 1007 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
972 1008 for rev in pr.revisions:
973 1009 pr_id = pr.pull_request_id
974 1010 pr_repo = pr.other_repo.repo_name
975 1011 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
976 1012
977 1013 for stat in statuses.all():
978 1014 pr_id = pr_repo = None
979 1015 if stat.pull_request:
980 1016 pr_id = stat.pull_request.pull_request_id
981 1017 pr_repo = stat.pull_request.other_repo.repo_name
982 1018 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
983 1019 pr_id, pr_repo]
984 1020 return grouped
985 1021
986 1022 #==========================================================================
987 1023 # SCM CACHE INSTANCE
988 1024 #==========================================================================
989 1025
990 1026 @property
991 1027 def invalidate(self):
992 1028 return CacheInvalidation.invalidate(self.repo_name)
993 1029
994 1030 def set_invalidate(self):
995 1031 """
996 1032 set a cache for invalidation for this instance
997 1033 """
998 1034 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
999 1035
1000 1036 @LazyProperty
1001 1037 def scm_instance(self):
1002 1038 import rhodecode
1003 1039 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1004 1040 if full_cache:
1005 1041 return self.scm_instance_cached()
1006 1042 return self.__get_instance()
1007 1043
1008 1044 def scm_instance_cached(self, cache_map=None):
1009 1045 @cache_region('long_term')
1010 1046 def _c(repo_name):
1011 1047 return self.__get_instance()
1012 1048 rn = self.repo_name
1013 1049 log.debug('Getting cached instance of repo')
1014 1050
1015 1051 if cache_map:
1016 1052 # get using prefilled cache_map
1017 1053 invalidate_repo = cache_map[self.repo_name]
1018 1054 if invalidate_repo:
1019 1055 invalidate_repo = (None if invalidate_repo.cache_active
1020 1056 else invalidate_repo)
1021 1057 else:
1022 1058 # get from invalidate
1023 1059 invalidate_repo = self.invalidate
1024 1060
1025 1061 if invalidate_repo is not None:
1026 1062 region_invalidate(_c, None, rn)
1027 1063 # update our cache
1028 1064 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1029 1065 return _c(rn)
1030 1066
1031 1067 def __get_instance(self):
1032 1068 repo_full_path = self.repo_full_path
1033 1069 try:
1034 1070 alias = get_scm(repo_full_path)[0]
1035 1071 log.debug('Creating instance of %s repository' % alias)
1036 1072 backend = get_backend(alias)
1037 1073 except VCSError:
1038 1074 log.error(traceback.format_exc())
1039 1075 log.error('Perhaps this repository is in db and not in '
1040 1076 'filesystem run rescan repositories with '
1041 1077 '"destroy old data " option from admin panel')
1042 1078 return
1043 1079
1044 1080 if alias == 'hg':
1045 1081
1046 1082 repo = backend(safe_str(repo_full_path), create=False,
1047 1083 baseui=self._ui)
1048 1084 # skip hidden web repository
1049 1085 if repo._get_hidden():
1050 1086 return
1051 1087 else:
1052 1088 repo = backend(repo_full_path, create=False)
1053 1089
1054 1090 return repo
1055 1091
1056 1092
1057 1093 class RepoGroup(Base, BaseModel):
1058 1094 __tablename__ = 'groups'
1059 1095 __table_args__ = (
1060 1096 UniqueConstraint('group_name', 'group_parent_id'),
1061 1097 CheckConstraint('group_id != group_parent_id'),
1062 1098 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1063 1099 'mysql_charset': 'utf8'},
1064 1100 )
1065 1101 __mapper_args__ = {'order_by': 'group_name'}
1066 1102
1067 1103 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1068 1104 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1069 1105 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1070 1106 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1071 1107 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1072 1108
1073 1109 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1074 1110 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
1075 1111
1076 1112 parent_group = relationship('RepoGroup', remote_side=group_id)
1077 1113
1078 1114 def __init__(self, group_name='', parent_group=None):
1079 1115 self.group_name = group_name
1080 1116 self.parent_group = parent_group
1081 1117
1082 1118 def __unicode__(self):
1083 1119 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1084 1120 self.group_name)
1085 1121
1086 1122 @classmethod
1087 1123 def groups_choices(cls, check_perms=False):
1088 1124 from webhelpers.html import literal as _literal
1089 1125 from rhodecode.model.scm import ScmModel
1090 1126 groups = cls.query().all()
1091 1127 if check_perms:
1092 1128 #filter group user have access to, it's done
1093 1129 #magically inside ScmModel based on current user
1094 1130 groups = ScmModel().get_repos_groups(groups)
1095 1131 repo_groups = [('', '')]
1096 1132 sep = ' &raquo; '
1097 1133 _name = lambda k: _literal(sep.join(k))
1098 1134
1099 1135 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1100 1136 for x in groups])
1101 1137
1102 1138 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1103 1139 return repo_groups
1104 1140
1105 1141 @classmethod
1106 1142 def url_sep(cls):
1107 1143 return URL_SEP
1108 1144
1109 1145 @classmethod
1110 1146 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1111 1147 if case_insensitive:
1112 1148 gr = cls.query()\
1113 1149 .filter(cls.group_name.ilike(group_name))
1114 1150 else:
1115 1151 gr = cls.query()\
1116 1152 .filter(cls.group_name == group_name)
1117 1153 if cache:
1118 1154 gr = gr.options(FromCache(
1119 1155 "sql_cache_short",
1120 1156 "get_group_%s" % _hash_key(group_name)
1121 1157 )
1122 1158 )
1123 1159 return gr.scalar()
1124 1160
1125 1161 @property
1126 1162 def parents(self):
1127 1163 parents_recursion_limit = 5
1128 1164 groups = []
1129 1165 if self.parent_group is None:
1130 1166 return groups
1131 1167 cur_gr = self.parent_group
1132 1168 groups.insert(0, cur_gr)
1133 1169 cnt = 0
1134 1170 while 1:
1135 1171 cnt += 1
1136 1172 gr = getattr(cur_gr, 'parent_group', None)
1137 1173 cur_gr = cur_gr.parent_group
1138 1174 if gr is None:
1139 1175 break
1140 1176 if cnt == parents_recursion_limit:
1141 1177 # this will prevent accidental infinit loops
1142 1178 log.error('group nested more than %s' %
1143 1179 parents_recursion_limit)
1144 1180 break
1145 1181
1146 1182 groups.insert(0, gr)
1147 1183 return groups
1148 1184
1149 1185 @property
1150 1186 def children(self):
1151 1187 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1152 1188
1153 1189 @property
1154 1190 def name(self):
1155 1191 return self.group_name.split(RepoGroup.url_sep())[-1]
1156 1192
1157 1193 @property
1158 1194 def full_path(self):
1159 1195 return self.group_name
1160 1196
1161 1197 @property
1162 1198 def full_path_splitted(self):
1163 1199 return self.group_name.split(RepoGroup.url_sep())
1164 1200
1165 1201 @property
1166 1202 def repositories(self):
1167 1203 return Repository.query()\
1168 1204 .filter(Repository.group == self)\
1169 1205 .order_by(Repository.repo_name)
1170 1206
1171 1207 @property
1172 1208 def repositories_recursive_count(self):
1173 1209 cnt = self.repositories.count()
1174 1210
1175 1211 def children_count(group):
1176 1212 cnt = 0
1177 1213 for child in group.children:
1178 1214 cnt += child.repositories.count()
1179 1215 cnt += children_count(child)
1180 1216 return cnt
1181 1217
1182 1218 return cnt + children_count(self)
1183 1219
1184 1220 def recursive_groups_and_repos(self):
1185 1221 """
1186 1222 Recursive return all groups, with repositories in those groups
1187 1223 """
1188 1224 all_ = []
1189 1225
1190 1226 def _get_members(root_gr):
1191 1227 for r in root_gr.repositories:
1192 1228 all_.append(r)
1193 1229 childs = root_gr.children.all()
1194 1230 if childs:
1195 1231 for gr in childs:
1196 1232 all_.append(gr)
1197 1233 _get_members(gr)
1198 1234
1199 1235 _get_members(self)
1200 1236 return [self] + all_
1201 1237
1202 1238 def get_new_name(self, group_name):
1203 1239 """
1204 1240 returns new full group name based on parent and new name
1205 1241
1206 1242 :param group_name:
1207 1243 """
1208 1244 path_prefix = (self.parent_group.full_path_splitted if
1209 1245 self.parent_group else [])
1210 1246 return RepoGroup.url_sep().join(path_prefix + [group_name])
1211 1247
1212 1248
1213 1249 class Permission(Base, BaseModel):
1214 1250 __tablename__ = 'permissions'
1215 1251 __table_args__ = (
1216 1252 Index('p_perm_name_idx', 'permission_name'),
1217 1253 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1218 1254 'mysql_charset': 'utf8'},
1219 1255 )
1220 1256 PERMS = [
1221 1257 ('repository.none', _('Repository no access')),
1222 1258 ('repository.read', _('Repository read access')),
1223 1259 ('repository.write', _('Repository write access')),
1224 1260 ('repository.admin', _('Repository admin access')),
1225 1261
1226 1262 ('group.none', _('Repositories Group no access')),
1227 1263 ('group.read', _('Repositories Group read access')),
1228 1264 ('group.write', _('Repositories Group write access')),
1229 1265 ('group.admin', _('Repositories Group admin access')),
1230 1266
1231 1267 ('hg.admin', _('RhodeCode Administrator')),
1232 1268 ('hg.create.none', _('Repository creation disabled')),
1233 1269 ('hg.create.repository', _('Repository creation enabled')),
1234 1270 ('hg.fork.none', _('Repository forking disabled')),
1235 1271 ('hg.fork.repository', _('Repository forking enabled')),
1236 1272 ('hg.register.none', _('Register disabled')),
1237 1273 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1238 1274 'with manual activation')),
1239 1275
1240 1276 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1241 1277 'with auto activation')),
1242 1278 ]
1243 1279
1244 1280 # defines which permissions are more important higher the more important
1245 1281 PERM_WEIGHTS = {
1246 1282 'repository.none': 0,
1247 1283 'repository.read': 1,
1248 1284 'repository.write': 3,
1249 1285 'repository.admin': 4,
1250 1286
1251 1287 'group.none': 0,
1252 1288 'group.read': 1,
1253 1289 'group.write': 3,
1254 1290 'group.admin': 4,
1255 1291
1256 1292 'hg.fork.none': 0,
1257 1293 'hg.fork.repository': 1,
1258 1294 'hg.create.none': 0,
1259 1295 'hg.create.repository':1
1260 1296 }
1261 1297
1262 1298 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1263 1299 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1264 1300 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1265 1301
1266 1302 def __unicode__(self):
1267 1303 return u"<%s('%s:%s')>" % (
1268 1304 self.__class__.__name__, self.permission_id, self.permission_name
1269 1305 )
1270 1306
1271 1307 @classmethod
1272 1308 def get_by_key(cls, key):
1273 1309 return cls.query().filter(cls.permission_name == key).scalar()
1274 1310
1275 1311 @classmethod
1276 1312 def get_default_perms(cls, default_user_id):
1277 1313 q = Session().query(UserRepoToPerm, Repository, cls)\
1278 1314 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1279 1315 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1280 1316 .filter(UserRepoToPerm.user_id == default_user_id)
1281 1317
1282 1318 return q.all()
1283 1319
1284 1320 @classmethod
1285 1321 def get_default_group_perms(cls, default_user_id):
1286 1322 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1287 1323 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1288 1324 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1289 1325 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1290 1326
1291 1327 return q.all()
1292 1328
1293 1329
1294 1330 class UserRepoToPerm(Base, BaseModel):
1295 1331 __tablename__ = 'repo_to_perm'
1296 1332 __table_args__ = (
1297 1333 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1298 1334 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1299 1335 'mysql_charset': 'utf8'}
1300 1336 )
1301 1337 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1302 1338 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1303 1339 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1304 1340 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1305 1341
1306 1342 user = relationship('User')
1307 1343 repository = relationship('Repository')
1308 1344 permission = relationship('Permission')
1309 1345
1310 1346 @classmethod
1311 1347 def create(cls, user, repository, permission):
1312 1348 n = cls()
1313 1349 n.user = user
1314 1350 n.repository = repository
1315 1351 n.permission = permission
1316 1352 Session().add(n)
1317 1353 return n
1318 1354
1319 1355 def __unicode__(self):
1320 1356 return u'<user:%s => %s >' % (self.user, self.repository)
1321 1357
1322 1358
1323 1359 class UserToPerm(Base, BaseModel):
1324 1360 __tablename__ = 'user_to_perm'
1325 1361 __table_args__ = (
1326 1362 UniqueConstraint('user_id', 'permission_id'),
1327 1363 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1328 1364 'mysql_charset': 'utf8'}
1329 1365 )
1330 1366 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1331 1367 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1332 1368 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1333 1369
1334 1370 user = relationship('User')
1335 1371 permission = relationship('Permission', lazy='joined')
1336 1372
1337 1373
1338 1374 class UsersGroupRepoToPerm(Base, BaseModel):
1339 1375 __tablename__ = 'users_group_repo_to_perm'
1340 1376 __table_args__ = (
1341 1377 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1342 1378 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1343 1379 'mysql_charset': 'utf8'}
1344 1380 )
1345 1381 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1346 1382 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1347 1383 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1348 1384 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1349 1385
1350 1386 users_group = relationship('UsersGroup')
1351 1387 permission = relationship('Permission')
1352 1388 repository = relationship('Repository')
1353 1389
1354 1390 @classmethod
1355 1391 def create(cls, users_group, repository, permission):
1356 1392 n = cls()
1357 1393 n.users_group = users_group
1358 1394 n.repository = repository
1359 1395 n.permission = permission
1360 1396 Session().add(n)
1361 1397 return n
1362 1398
1363 1399 def __unicode__(self):
1364 1400 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1365 1401
1366 1402
1367 1403 class UsersGroupToPerm(Base, BaseModel):
1368 1404 __tablename__ = 'users_group_to_perm'
1369 1405 __table_args__ = (
1370 1406 UniqueConstraint('users_group_id', 'permission_id',),
1371 1407 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1372 1408 'mysql_charset': 'utf8'}
1373 1409 )
1374 1410 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1375 1411 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1376 1412 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1377 1413
1378 1414 users_group = relationship('UsersGroup')
1379 1415 permission = relationship('Permission')
1380 1416
1381 1417
1382 1418 class UserRepoGroupToPerm(Base, BaseModel):
1383 1419 __tablename__ = 'user_repo_group_to_perm'
1384 1420 __table_args__ = (
1385 1421 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1386 1422 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1387 1423 'mysql_charset': 'utf8'}
1388 1424 )
1389 1425
1390 1426 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1391 1427 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1392 1428 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1393 1429 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1394 1430
1395 1431 user = relationship('User')
1396 1432 group = relationship('RepoGroup')
1397 1433 permission = relationship('Permission')
1398 1434
1399 1435
1400 1436 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1401 1437 __tablename__ = 'users_group_repo_group_to_perm'
1402 1438 __table_args__ = (
1403 1439 UniqueConstraint('users_group_id', 'group_id'),
1404 1440 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1405 1441 'mysql_charset': 'utf8'}
1406 1442 )
1407 1443
1408 1444 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)
1409 1445 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1410 1446 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1411 1447 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1412 1448
1413 1449 users_group = relationship('UsersGroup')
1414 1450 permission = relationship('Permission')
1415 1451 group = relationship('RepoGroup')
1416 1452
1417 1453
1418 1454 class Statistics(Base, BaseModel):
1419 1455 __tablename__ = 'statistics'
1420 1456 __table_args__ = (
1421 1457 UniqueConstraint('repository_id'),
1422 1458 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1423 1459 'mysql_charset': 'utf8'}
1424 1460 )
1425 1461 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1426 1462 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1427 1463 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1428 1464 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1429 1465 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1430 1466 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1431 1467
1432 1468 repository = relationship('Repository', single_parent=True)
1433 1469
1434 1470
1435 1471 class UserFollowing(Base, BaseModel):
1436 1472 __tablename__ = 'user_followings'
1437 1473 __table_args__ = (
1438 1474 UniqueConstraint('user_id', 'follows_repository_id'),
1439 1475 UniqueConstraint('user_id', 'follows_user_id'),
1440 1476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1441 1477 'mysql_charset': 'utf8'}
1442 1478 )
1443 1479
1444 1480 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1445 1481 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1446 1482 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1447 1483 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1448 1484 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1449 1485
1450 1486 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1451 1487
1452 1488 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1453 1489 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1454 1490
1455 1491 @classmethod
1456 1492 def get_repo_followers(cls, repo_id):
1457 1493 return cls.query().filter(cls.follows_repo_id == repo_id)
1458 1494
1459 1495
1460 1496 class CacheInvalidation(Base, BaseModel):
1461 1497 __tablename__ = 'cache_invalidation'
1462 1498 __table_args__ = (
1463 1499 UniqueConstraint('cache_key'),
1464 1500 Index('key_idx', 'cache_key'),
1465 1501 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1466 1502 'mysql_charset': 'utf8'},
1467 1503 )
1468 1504 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1469 1505 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1470 1506 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1471 1507 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1472 1508
1473 1509 def __init__(self, cache_key, cache_args=''):
1474 1510 self.cache_key = cache_key
1475 1511 self.cache_args = cache_args
1476 1512 self.cache_active = False
1477 1513
1478 1514 def __unicode__(self):
1479 1515 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1480 1516 self.cache_id, self.cache_key)
1481 1517
1482 1518 @property
1483 1519 def prefix(self):
1484 1520 _split = self.cache_key.split(self.cache_args, 1)
1485 1521 if _split and len(_split) == 2:
1486 1522 return _split[0]
1487 1523 return ''
1488 1524
1489 1525 @classmethod
1490 1526 def clear_cache(cls):
1491 1527 cls.query().delete()
1492 1528
1493 1529 @classmethod
1494 1530 def _get_key(cls, key):
1495 1531 """
1496 1532 Wrapper for generating a key, together with a prefix
1497 1533
1498 1534 :param key:
1499 1535 """
1500 1536 import rhodecode
1501 1537 prefix = ''
1502 1538 org_key = key
1503 1539 iid = rhodecode.CONFIG.get('instance_id')
1504 1540 if iid:
1505 1541 prefix = iid
1506 1542
1507 1543 return "%s%s" % (prefix, key), prefix, org_key
1508 1544
1509 1545 @classmethod
1510 1546 def get_by_key(cls, key):
1511 1547 return cls.query().filter(cls.cache_key == key).scalar()
1512 1548
1513 1549 @classmethod
1514 1550 def get_by_repo_name(cls, repo_name):
1515 1551 return cls.query().filter(cls.cache_args == repo_name).all()
1516 1552
1517 1553 @classmethod
1518 1554 def _get_or_create_key(cls, key, repo_name, commit=True):
1519 1555 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1520 1556 if not inv_obj:
1521 1557 try:
1522 1558 inv_obj = CacheInvalidation(key, repo_name)
1523 1559 Session().add(inv_obj)
1524 1560 if commit:
1525 1561 Session().commit()
1526 1562 except Exception:
1527 1563 log.error(traceback.format_exc())
1528 1564 Session().rollback()
1529 1565 return inv_obj
1530 1566
1531 1567 @classmethod
1532 1568 def invalidate(cls, key):
1533 1569 """
1534 1570 Returns Invalidation object if this given key should be invalidated
1535 1571 None otherwise. `cache_active = False` means that this cache
1536 1572 state is not valid and needs to be invalidated
1537 1573
1538 1574 :param key:
1539 1575 """
1540 1576 repo_name = key
1541 1577 repo_name = remove_suffix(repo_name, '_README')
1542 1578 repo_name = remove_suffix(repo_name, '_RSS')
1543 1579 repo_name = remove_suffix(repo_name, '_ATOM')
1544 1580
1545 1581 # adds instance prefix
1546 1582 key, _prefix, _org_key = cls._get_key(key)
1547 1583 inv = cls._get_or_create_key(key, repo_name)
1548 1584
1549 1585 if inv and inv.cache_active is False:
1550 1586 return inv
1551 1587
1552 1588 @classmethod
1553 1589 def set_invalidate(cls, key=None, repo_name=None):
1554 1590 """
1555 1591 Mark this Cache key for invalidation, either by key or whole
1556 1592 cache sets based on repo_name
1557 1593
1558 1594 :param key:
1559 1595 """
1560 1596 if key:
1561 1597 key, _prefix, _org_key = cls._get_key(key)
1562 1598 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1563 1599 elif repo_name:
1564 1600 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1565 1601
1566 1602 log.debug('marking %s key[s] for invalidation based on key=%s,repo_name=%s'
1567 1603 % (len(inv_objs), key, repo_name))
1568 1604 try:
1569 1605 for inv_obj in inv_objs:
1570 1606 inv_obj.cache_active = False
1571 1607 Session().add(inv_obj)
1572 1608 Session().commit()
1573 1609 except Exception:
1574 1610 log.error(traceback.format_exc())
1575 1611 Session().rollback()
1576 1612
1577 1613 @classmethod
1578 1614 def set_valid(cls, key):
1579 1615 """
1580 1616 Mark this cache key as active and currently cached
1581 1617
1582 1618 :param key:
1583 1619 """
1584 1620 inv_obj = cls.get_by_key(key)
1585 1621 inv_obj.cache_active = True
1586 1622 Session().add(inv_obj)
1587 1623 Session().commit()
1588 1624
1589 1625 @classmethod
1590 1626 def get_cache_map(cls):
1591 1627
1592 1628 class cachemapdict(dict):
1593 1629
1594 1630 def __init__(self, *args, **kwargs):
1595 1631 fixkey = kwargs.get('fixkey')
1596 1632 if fixkey:
1597 1633 del kwargs['fixkey']
1598 1634 self.fixkey = fixkey
1599 1635 super(cachemapdict, self).__init__(*args, **kwargs)
1600 1636
1601 1637 def __getattr__(self, name):
1602 1638 key = name
1603 1639 if self.fixkey:
1604 1640 key, _prefix, _org_key = cls._get_key(key)
1605 1641 if key in self.__dict__:
1606 1642 return self.__dict__[key]
1607 1643 else:
1608 1644 return self[key]
1609 1645
1610 1646 def __getitem__(self, key):
1611 1647 if self.fixkey:
1612 1648 key, _prefix, _org_key = cls._get_key(key)
1613 1649 try:
1614 1650 return super(cachemapdict, self).__getitem__(key)
1615 1651 except KeyError:
1616 1652 return
1617 1653
1618 1654 cache_map = cachemapdict(fixkey=True)
1619 1655 for obj in cls.query().all():
1620 1656 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1621 1657 return cache_map
1622 1658
1623 1659
1624 1660 class ChangesetComment(Base, BaseModel):
1625 1661 __tablename__ = 'changeset_comments'
1626 1662 __table_args__ = (
1627 1663 Index('cc_revision_idx', 'revision'),
1628 1664 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1629 1665 'mysql_charset': 'utf8'},
1630 1666 )
1631 1667 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1632 1668 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1633 1669 revision = Column('revision', String(40), nullable=True)
1634 1670 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1635 1671 line_no = Column('line_no', Unicode(10), nullable=True)
1636 1672 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1637 1673 f_path = Column('f_path', Unicode(1000), nullable=True)
1638 1674 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1639 1675 text = Column('text', UnicodeText(25000), nullable=False)
1640 1676 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1641 1677 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1642 1678
1643 1679 author = relationship('User', lazy='joined')
1644 1680 repo = relationship('Repository')
1645 1681 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1646 1682 pull_request = relationship('PullRequest', lazy='joined')
1647 1683
1648 1684 @classmethod
1649 1685 def get_users(cls, revision=None, pull_request_id=None):
1650 1686 """
1651 1687 Returns user associated with this ChangesetComment. ie those
1652 1688 who actually commented
1653 1689
1654 1690 :param cls:
1655 1691 :param revision:
1656 1692 """
1657 1693 q = Session().query(User)\
1658 1694 .join(ChangesetComment.author)
1659 1695 if revision:
1660 1696 q = q.filter(cls.revision == revision)
1661 1697 elif pull_request_id:
1662 1698 q = q.filter(cls.pull_request_id == pull_request_id)
1663 1699 return q.all()
1664 1700
1665 1701
1666 1702 class ChangesetStatus(Base, BaseModel):
1667 1703 __tablename__ = 'changeset_statuses'
1668 1704 __table_args__ = (
1669 1705 Index('cs_revision_idx', 'revision'),
1670 1706 Index('cs_version_idx', 'version'),
1671 1707 UniqueConstraint('repo_id', 'revision', 'version'),
1672 1708 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1673 1709 'mysql_charset': 'utf8'}
1674 1710 )
1675 1711 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1676 1712 STATUS_APPROVED = 'approved'
1677 1713 STATUS_REJECTED = 'rejected'
1678 1714 STATUS_UNDER_REVIEW = 'under_review'
1679 1715
1680 1716 STATUSES = [
1681 1717 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1682 1718 (STATUS_APPROVED, _("Approved")),
1683 1719 (STATUS_REJECTED, _("Rejected")),
1684 1720 (STATUS_UNDER_REVIEW, _("Under Review")),
1685 1721 ]
1686 1722
1687 1723 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1688 1724 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1689 1725 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1690 1726 revision = Column('revision', String(40), nullable=False)
1691 1727 status = Column('status', String(128), nullable=False, default=DEFAULT)
1692 1728 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1693 1729 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1694 1730 version = Column('version', Integer(), nullable=False, default=0)
1695 1731 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1696 1732
1697 1733 author = relationship('User', lazy='joined')
1698 1734 repo = relationship('Repository')
1699 1735 comment = relationship('ChangesetComment', lazy='joined')
1700 1736 pull_request = relationship('PullRequest', lazy='joined')
1701 1737
1702 1738 def __unicode__(self):
1703 1739 return u"<%s('%s:%s')>" % (
1704 1740 self.__class__.__name__,
1705 1741 self.status, self.author
1706 1742 )
1707 1743
1708 1744 @classmethod
1709 1745 def get_status_lbl(cls, value):
1710 1746 return dict(cls.STATUSES).get(value)
1711 1747
1712 1748 @property
1713 1749 def status_lbl(self):
1714 1750 return ChangesetStatus.get_status_lbl(self.status)
1715 1751
1716 1752
1717 1753 class PullRequest(Base, BaseModel):
1718 1754 __tablename__ = 'pull_requests'
1719 1755 __table_args__ = (
1720 1756 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1721 1757 'mysql_charset': 'utf8'},
1722 1758 )
1723 1759
1724 1760 STATUS_NEW = u'new'
1725 1761 STATUS_OPEN = u'open'
1726 1762 STATUS_CLOSED = u'closed'
1727 1763
1728 1764 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1729 1765 title = Column('title', Unicode(256), nullable=True)
1730 1766 description = Column('description', UnicodeText(10240), nullable=True)
1731 1767 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1732 1768 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1733 1769 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1734 1770 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1735 1771 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1736 1772 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1737 1773 org_ref = Column('org_ref', Unicode(256), nullable=False)
1738 1774 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1739 1775 other_ref = Column('other_ref', Unicode(256), nullable=False)
1740 1776
1741 1777 @hybrid_property
1742 1778 def revisions(self):
1743 1779 return self._revisions.split(':')
1744 1780
1745 1781 @revisions.setter
1746 1782 def revisions(self, val):
1747 1783 self._revisions = ':'.join(val)
1748 1784
1749 1785 author = relationship('User', lazy='joined')
1750 1786 reviewers = relationship('PullRequestReviewers',
1751 1787 cascade="all, delete, delete-orphan")
1752 1788 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1753 1789 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1754 1790 statuses = relationship('ChangesetStatus')
1755 1791 comments = relationship('ChangesetComment',
1756 1792 cascade="all, delete, delete-orphan")
1757 1793
1758 1794 def is_closed(self):
1759 1795 return self.status == self.STATUS_CLOSED
1760 1796
1761 1797 def __json__(self):
1762 1798 return dict(
1763 1799 revisions=self.revisions
1764 1800 )
1765 1801
1766 1802
1767 1803 class PullRequestReviewers(Base, BaseModel):
1768 1804 __tablename__ = 'pull_request_reviewers'
1769 1805 __table_args__ = (
1770 1806 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1771 1807 'mysql_charset': 'utf8'},
1772 1808 )
1773 1809
1774 1810 def __init__(self, user=None, pull_request=None):
1775 1811 self.user = user
1776 1812 self.pull_request = pull_request
1777 1813
1778 1814 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1779 1815 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1780 1816 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1781 1817
1782 1818 user = relationship('User')
1783 1819 pull_request = relationship('PullRequest')
1784 1820
1785 1821
1786 1822 class Notification(Base, BaseModel):
1787 1823 __tablename__ = 'notifications'
1788 1824 __table_args__ = (
1789 1825 Index('notification_type_idx', 'type'),
1790 1826 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1791 1827 'mysql_charset': 'utf8'},
1792 1828 )
1793 1829
1794 1830 TYPE_CHANGESET_COMMENT = u'cs_comment'
1795 1831 TYPE_MESSAGE = u'message'
1796 1832 TYPE_MENTION = u'mention'
1797 1833 TYPE_REGISTRATION = u'registration'
1798 1834 TYPE_PULL_REQUEST = u'pull_request'
1799 1835 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1800 1836
1801 1837 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1802 1838 subject = Column('subject', Unicode(512), nullable=True)
1803 1839 body = Column('body', UnicodeText(50000), nullable=True)
1804 1840 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1805 1841 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1806 1842 type_ = Column('type', Unicode(256))
1807 1843
1808 1844 created_by_user = relationship('User')
1809 1845 notifications_to_users = relationship('UserNotification', lazy='joined',
1810 1846 cascade="all, delete, delete-orphan")
1811 1847
1812 1848 @property
1813 1849 def recipients(self):
1814 1850 return [x.user for x in UserNotification.query()\
1815 1851 .filter(UserNotification.notification == self)\
1816 1852 .order_by(UserNotification.user_id.asc()).all()]
1817 1853
1818 1854 @classmethod
1819 1855 def create(cls, created_by, subject, body, recipients, type_=None):
1820 1856 if type_ is None:
1821 1857 type_ = Notification.TYPE_MESSAGE
1822 1858
1823 1859 notification = cls()
1824 1860 notification.created_by_user = created_by
1825 1861 notification.subject = subject
1826 1862 notification.body = body
1827 1863 notification.type_ = type_
1828 1864 notification.created_on = datetime.datetime.now()
1829 1865
1830 1866 for u in recipients:
1831 1867 assoc = UserNotification()
1832 1868 assoc.notification = notification
1833 1869 u.notifications.append(assoc)
1834 1870 Session().add(notification)
1835 1871 return notification
1836 1872
1837 1873 @property
1838 1874 def description(self):
1839 1875 from rhodecode.model.notification import NotificationModel
1840 1876 return NotificationModel().make_description(self)
1841 1877
1842 1878
1843 1879 class UserNotification(Base, BaseModel):
1844 1880 __tablename__ = 'user_to_notification'
1845 1881 __table_args__ = (
1846 1882 UniqueConstraint('user_id', 'notification_id'),
1847 1883 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1848 1884 'mysql_charset': 'utf8'}
1849 1885 )
1850 1886 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1851 1887 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1852 1888 read = Column('read', Boolean, default=False)
1853 1889 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1854 1890
1855 1891 user = relationship('User', lazy="joined")
1856 1892 notification = relationship('Notification', lazy="joined",
1857 1893 order_by=lambda: Notification.created_on.desc(),)
1858 1894
1859 1895 def mark_as_read(self):
1860 1896 self.read = True
1861 1897 Session().add(self)
1862 1898
1863 1899
1864 1900 class DbMigrateVersion(Base, BaseModel):
1865 1901 __tablename__ = 'db_migrate_version'
1866 1902 __table_args__ = (
1867 1903 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1868 1904 'mysql_charset': 'utf8'},
1869 1905 )
1870 1906 repository_id = Column('repository_id', String(250), primary_key=True)
1871 1907 repository_path = Column('repository_path', Text)
1872 1908 version = Column('version', Integer)
@@ -1,331 +1,334 b''
1 1 <%page args="parent" />
2 2 <div class="box">
3 3 <!-- box / title -->
4 4 <div class="title">
5 5 <h5>
6 6 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
7 7 </h5>
8 8 %if c.rhodecode_user.username != 'default':
9 9 %if h.HasPermissionAny('hg.admin','hg.create.repository')():
10 10 <ul class="links">
11 11 <li>
12 12 %if c.group:
13 13 <span>${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
14 14 %else:
15 15 <span>${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository'))}</span>
16 16 %endif
17 17 </li>
18 18 </ul>
19 19 %endif
20 20 %endif
21 21 </div>
22 22 <!-- end box / title -->
23 23 <div class="table">
24 24 % if c.groups:
25 25 <div id='groups_list_wrap' class="yui-skin-sam">
26 26 <table id="groups_list">
27 27 <thead>
28 28 <tr>
29 29 <th class="left"><a href="#">${_('Group name')}</a></th>
30 30 <th class="left"><a href="#">${_('Description')}</a></th>
31 31 ##<th class="left"><a href="#">${_('Number of repositories')}</a></th>
32 32 </tr>
33 33 </thead>
34 34
35 35 ## REPO GROUPS
36 36 % for gr in c.groups:
37 37 <tr>
38 38 <td>
39 39 <div style="white-space: nowrap">
40 40 <img class="icon" alt="${_('Repositories group')}" src="${h.url('/images/icons/database_link.png')}"/>
41 41 ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))}
42 42 </div>
43 43 </td>
44 44 %if c.visual.stylify_metatags:
45 45 <td>${h.urlify_text(h.desc_stylize(gr.group_description))}</td>
46 46 %else:
47 47 <td>${gr.group_description}</td>
48 48 %endif
49 49 ## this is commented out since for multi nested repos can be HEAVY!
50 50 ## in number of executed queries during traversing uncomment at will
51 51 ##<td><b>${gr.repositories_recursive_count}</b></td>
52 52 </tr>
53 53 % endfor
54 54
55 55 </table>
56 56 </div>
57 57 <div style="height: 20px"></div>
58 58 % endif
59 59 <div id="welcome" style="display:none;text-align:center">
60 60 <h1><a href="${h.url('home')}">${c.rhodecode_name} ${c.rhodecode_version}</a></h1>
61 61 </div>
62 62 <%cnt=0%>
63 63 <%namespace name="dt" file="/data_table/_dt_elements.html"/>
64 64 % if c.visual.lightweight_dashboard is False:
65 65 ## old full detailed version
66 66 <div id='repos_list_wrap' class="yui-skin-sam">
67 67 <table id="repos_list">
68 68 <thead>
69 69 <tr>
70 70 <th class="left"></th>
71 71 <th class="left">${_('Name')}</th>
72 72 <th class="left">${_('Description')}</th>
73 73 <th class="left">${_('Last change')}</th>
74 74 <th class="left">${_('Tip')}</th>
75 75 <th class="left">${_('Owner')}</th>
76 76 <th class="left">${_('RSS')}</th>
77 77 <th class="left">${_('Atom')}</th>
78 78 </tr>
79 79 </thead>
80 80 <tbody>
81 81 %for cnt,repo in enumerate(c.repos_list):
82 82 <tr class="parity${(cnt+1)%2}">
83 83 ##QUICK MENU
84 84 <td class="quick_repo_menu">
85 85 ${dt.quick_menu(repo['name'])}
86 86 </td>
87 87 ##REPO NAME AND ICONS
88 88 <td class="reponame">
89 89 ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']),pageargs.get('short_repo_names'))}
90 90 </td>
91 91 ##DESCRIPTION
92 92 <td><span class="tooltip" title="${h.tooltip(repo['description'])}">
93 93 %if c.visual.stylify_metatags:
94 94 ${h.urlify_text(h.desc_stylize(h.truncate(repo['description'],60)))}</span>
95 95 %else:
96 96 ${h.truncate(repo['description'],60)}</span>
97 97 %endif
98 98 </td>
99 99 ##LAST CHANGE DATE
100 100 <td>
101 101 ${dt.last_change(repo['last_change'])}
102 102 </td>
103 103 ##LAST REVISION
104 104 <td>
105 105 ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])}
106 106 </td>
107 107 ##
108 108 <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
109 109 <td>
110 110 ${dt.rss(repo['name'])}
111 111 </td>
112 112 <td>
113 113 ${dt.atom(repo['name'])}
114 114 </td>
115 115 </tr>
116 116 %endfor
117 117 </tbody>
118 118 </table>
119 119 </div>
120 120 % else:
121 121 ## lightweight version
122 122 <div class="yui-skin-sam" id="repos_list_wrap"></div>
123 123 <div id="user-paginator" style="padding: 0px 0px 0px 0px"></div>
124 124 % endif
125 125 </div>
126 126 </div>
127 127 % if c.visual.lightweight_dashboard is False:
128 128 <script>
129 129 YUD.get('repo_count').innerHTML = ${cnt+1 if cnt else 0};
130 130 var func = function(node){
131 131 return node.parentNode.parentNode.parentNode.parentNode;
132 132 }
133 133
134 134 // groups table sorting
135 135 var myColumnDefs = [
136 136 {key:"name",label:"${_('Group name')}",sortable:true,
137 137 sortOptions: { sortFunction: groupNameSort }},
138 138 {key:"desc",label:"${_('Description')}",sortable:true},
139 139 ];
140 140
141 141 var myDataSource = new YAHOO.util.DataSource(YUD.get("groups_list"));
142 142
143 143 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
144 144 myDataSource.responseSchema = {
145 145 fields: [
146 146 {key:"name"},
147 147 {key:"desc"},
148 148 ]
149 149 };
150 150
151 151 var myDataTable = new YAHOO.widget.DataTable("groups_list_wrap", myColumnDefs, myDataSource,{
152 152 sortedBy:{key:"name",dir:"asc"},
153 153 paginator: new YAHOO.widget.Paginator({
154 154 rowsPerPage: 5,
155 155 alwaysVisible: false,
156 156 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
157 157 pageLinks: 5,
158 158 containerClass: 'pagination-wh',
159 159 currentPageClass: 'pager_curpage',
160 160 pageLinkClass: 'pager_link',
161 161 nextPageLinkLabel: '&gt;',
162 162 previousPageLinkLabel: '&lt;',
163 163 firstPageLinkLabel: '&lt;&lt;',
164 164 lastPageLinkLabel: '&gt;&gt;',
165 165 containers:['user-paginator']
166 166 }),
167 167 MSG_SORTASC:"${_('Click to sort ascending')}",
168 168 MSG_SORTDESC:"${_('Click to sort descending')}"
169 169 });
170 170
171 171 // main table sorting
172 172 var myColumnDefs = [
173 173 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
174 174 {key:"name",label:"${_('Name')}",sortable:true,
175 175 sortOptions: { sortFunction: nameSort }},
176 176 {key:"desc",label:"${_('Description')}",sortable:true},
177 177 {key:"last_change",label:"${_('Last Change')}",sortable:true,
178 178 sortOptions: { sortFunction: ageSort }},
179 179 {key:"tip",label:"${_('Tip')}",sortable:true,
180 180 sortOptions: { sortFunction: revisionSort }},
181 181 {key:"owner",label:"${_('Owner')}",sortable:true},
182 182 {key:"rss",label:"",sortable:false},
183 183 {key:"atom",label:"",sortable:false},
184 184 ];
185 185
186 186 var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
187 187
188 188 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
189 189
190 190 myDataSource.responseSchema = {
191 191 fields: [
192 192 {key:"menu"},
193 193 //{key:"raw_name"},
194 194 {key:"name"},
195 195 {key:"desc"},
196 196 {key:"last_change"},
197 197 {key:"tip"},
198 198 {key:"owner"},
199 199 {key:"rss"},
200 200 {key:"atom"},
201 201 ]
202 202 };
203 203
204 204 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
205 205 {
206 206 sortedBy:{key:"name",dir:"asc"},
207 207 MSG_SORTASC:"${_('Click to sort ascending')}",
208 208 MSG_SORTDESC:"${_('Click to sort descending')}",
209 209 MSG_EMPTY:"${_('No records found.')}",
210 210 MSG_ERROR:"${_('Data error.')}",
211 211 MSG_LOADING:"${_('Loading...')}",
212 212 }
213 213 );
214 214 myDataTable.subscribe('postRenderEvent',function(oArgs) {
215 215 tooltip_activate();
216 216 quick_repo_menu();
217 217 q_filter('q_filter',YUQ('div.table tr td a.repo_name'),func);
218 218 });
219 219
220 220 </script>
221 221 % else:
222 222 <script>
223 223 //var url = "${h.url('formatted_users', format='json')}";
224 224 var data = ${c.data|n};
225 225 var myDataSource = new YAHOO.util.DataSource(data);
226 226 myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
227 227
228 228 myDataSource.responseSchema = {
229 229 resultsList: "records",
230 230 fields: [
231 231 {key:"menu"},
232 232 {key:"raw_name"},
233 233 {key:"name"},
234 234 {key:"desc"},
235 235 {key:"last_change"},
236 {key: "tip"},
236 237 {key:"owner"},
237 238 {key:"rss"},
238 239 {key:"atom"},
239 240 ]
240 241 };
241 242 myDataSource.doBeforeCallback = function(req,raw,res,cb) {
242 243 // This is the filter function
243 244 var data = res.results || [],
244 245 filtered = [],
245 246 i,l;
246 247
247 248 if (req) {
248 249 req = req.toLowerCase();
249 250 for (i = 0; i<data.length; i++) {
250 251 var pos = data[i].raw_name.toLowerCase().indexOf(req)
251 252 if (pos != -1) {
252 253 filtered.push(data[i]);
253 254 }
254 255 }
255 256 res.results = filtered;
256 257 }
257 258 YUD.get('repo_count').innerHTML = res.results.length;
258 259 return res;
259 260 }
260 261
261 262 // main table sorting
262 263 var myColumnDefs = [
263 264 {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
264 265 {key:"name",label:"${_('Name')}",sortable:true,
265 266 sortOptions: { sortFunction: nameSort }},
266 267 {key:"desc",label:"${_('Description')}",sortable:true},
267 268 {key:"last_change",label:"${_('Last Change')}",sortable:true,
268 269 sortOptions: { sortFunction: ageSort }},
270 {key:"tip",label:"${_('Tip')}",sortable:true,
271 sortOptions: { sortFunction: revisionSort }},
269 272 {key:"owner",label:"${_('Owner')}",sortable:true},
270 273 {key:"rss",label:"",sortable:false},
271 274 {key:"atom",label:"",sortable:false},
272 275 ];
273 276
274 277 var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{
275 278 sortedBy:{key:"name",dir:"asc"},
276 279 paginator: new YAHOO.widget.Paginator({
277 280 rowsPerPage: ${c.visual.lightweight_dashboard_items},
278 281 alwaysVisible: false,
279 282 template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}",
280 283 pageLinks: 5,
281 284 containerClass: 'pagination-wh',
282 285 currentPageClass: 'pager_curpage',
283 286 pageLinkClass: 'pager_link',
284 287 nextPageLinkLabel: '&gt;',
285 288 previousPageLinkLabel: '&lt;',
286 289 firstPageLinkLabel: '&lt;&lt;',
287 290 lastPageLinkLabel: '&gt;&gt;',
288 291 containers:['user-paginator']
289 292 }),
290 293
291 294 MSG_SORTASC:"${_('Click to sort ascending')}",
292 295 MSG_SORTDESC:"${_('Click to sort descending')}",
293 296 MSG_EMPTY:"${_('No records found.')}",
294 297 MSG_ERROR:"${_('Data error.')}",
295 298 MSG_LOADING:"${_('Loading...')}",
296 299 }
297 300 );
298 301 myDataTable.subscribe('postRenderEvent',function(oArgs) {
299 302 tooltip_activate();
300 303 quick_repo_menu();
301 304 });
302 305
303 306 var filterTimeout = null;
304 307
305 308 updateFilter = function () {
306 309 // Reset timeout
307 310 filterTimeout = null;
308 311
309 312 // Reset sort
310 313 var state = myDataTable.getState();
311 314 state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC};
312 315
313 316 // Get filtered data
314 317 myDataSource.sendRequest(YUD.get('q_filter').value,{
315 318 success : myDataTable.onDataReturnInitializeTable,
316 319 failure : myDataTable.onDataReturnInitializeTable,
317 320 scope : myDataTable,
318 321 argument: state
319 322 });
320 323
321 324 };
322 325 YUE.on('q_filter','click',function(){
323 326 YUD.get('q_filter').value = '';
324 327 });
325 328
326 329 YUE.on('q_filter','keyup',function (e) {
327 330 clearTimeout(filterTimeout);
328 331 filterTimeout = setTimeout(updateFilter,600);
329 332 });
330 333 </script>
331 334 % endif
@@ -1,45 +1,45 b''
1 1 [egg_info]
2 2 tag_build =
3 3 tag_svn_revision = true
4 4
5 5 [easy_install]
6 6 find_links = http://www.pylonshq.com/download/
7 7
8 8 [nosetests]
9 9 verbose=True
10 10 verbosity=2
11 11 with-pylons=test.ini
12 12 detailed-errors=1
13 13 nologcapture=1
14 #pdb=1
15 #pdb-failures=1
14 pdb=1
15 pdb-failures=1
16 16
17 17 # Babel configuration
18 18 [compile_catalog]
19 19 domain = rhodecode
20 20 directory = rhodecode/i18n
21 21 statistics = true
22 22
23 23 [extract_messages]
24 24 add_comments = TRANSLATORS:
25 25 output_file = rhodecode/i18n/rhodecode.pot
26 26 width = 80
27 27
28 28 [init_catalog]
29 29 domain = rhodecode
30 30 input_file = rhodecode/i18n/rhodecode.pot
31 31 output_dir = rhodecode/i18n
32 32
33 33 [update_catalog]
34 34 domain = rhodecode
35 35 input_file = rhodecode/i18n/rhodecode.pot
36 36 output_dir = rhodecode/i18n
37 37 previous = true
38 38
39 39 [build_sphinx]
40 40 source-dir = docs/
41 41 build-dir = docs/_build
42 42 all_files = 1
43 43
44 44 [upload_sphinx]
45 45 upload-dir = docs/_build/html No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now