##// END OF EJS Templates
Unicode fixes, added safe_str method for global str() operations +better test sandboxing
marcink -
r1401:b7563ad4 beta
parent child Browse files
Show More
@@ -1,80 +1,80 b''
1 1 """Pylons environment configuration"""
2 2
3 3 import os
4 4 import logging
5 5
6 6 from mako.lookup import TemplateLookup
7 7 from pylons.configuration import PylonsConfig
8 8 from pylons.error import handle_mako_error
9 9
10 10 import rhodecode.lib.app_globals as app_globals
11 11 import rhodecode.lib.helpers
12 12
13 13 from rhodecode.config.routing import make_map
14 14 from rhodecode.lib import celerypylons
15 15 from rhodecode.lib import engine_from_config
16 16 from rhodecode.lib.timerproxy import TimerProxy
17 17 from rhodecode.lib.auth import set_available_permissions
18 18 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config
19 19 from rhodecode.model import init_model
20 20 from rhodecode.model.scm import ScmModel
21 21
22 22 log = logging.getLogger(__name__)
23 23
24 24
25 25 def load_environment(global_conf, app_conf, initial=False):
26 26 """Configure the Pylons environment via the ``pylons.config``
27 27 object
28 28 """
29 29 config = PylonsConfig()
30 30
31 31 # Pylons paths
32 32 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
33 33 paths = dict(root=root,
34 34 controllers=os.path.join(root, 'controllers'),
35 35 static_files=os.path.join(root, 'public'),
36 36 templates=[os.path.join(root, 'templates')])
37 37
38 38 # Initialize config with the basic options
39 39 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
40 40
41 41 config['routes.map'] = make_map(config)
42 42 config['pylons.app_globals'] = app_globals.Globals(config)
43 43 config['pylons.h'] = rhodecode.lib.helpers
44 44
45 45 # Setup cache object as early as possible
46 46 import pylons
47 47 pylons.cache._push_object(config['pylons.app_globals'].cache)
48 48
49 49 # Create the Mako TemplateLookup, with the default auto-escaping
50 50 config['pylons.app_globals'].mako_lookup = TemplateLookup(
51 51 directories=paths['templates'],
52 52 error_handler=handle_mako_error,
53 53 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
54 54 input_encoding='utf-8', default_filters=['escape'],
55 55 imports=['from webhelpers.html import escape'])
56 56
57 57 #sets the c attribute access when don't existing attribute are accessed
58 58 config['pylons.strict_tmpl_context'] = True
59 59 test = os.path.split(config['__file__'])[-1] == 'test.ini'
60 60 if test:
61 61 from rhodecode.lib.utils import create_test_env, create_test_index
62 62 from rhodecode.tests import TESTS_TMP_PATH
63 63 create_test_env(TESTS_TMP_PATH, config)
64 create_test_index(TESTS_TMP_PATH, True)
64 create_test_index(TESTS_TMP_PATH, config, True)
65 65
66 66 #MULTIPLE DB configs
67 67 # Setup the SQLAlchemy database engine
68 68 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
69 69
70 70 init_model(sa_engine_db1)
71 71
72 72 repos_path = make_ui('db').configitems('paths')[0][1]
73 73 repo2db_mapper(ScmModel().repo_scan(repos_path))
74 74 set_available_permissions(config)
75 75 config['base_path'] = repos_path
76 76 set_rhodecode_config(config)
77 77 # CONFIGURATION OPTIONS HERE (note: all config options will override
78 78 # any Pylons config options)
79 79
80 80 return config
@@ -1,414 +1,414 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import mimetypes
29 29 import traceback
30 30
31 31 from pylons import request, response, session, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34
35 35 from vcs.backends import ARCHIVE_SPECS
36 36 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
37 37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError
38 38 from vcs.nodes import FileNode, NodeKind
39 39 from vcs.utils import diffs as differ
40 40
41 from rhodecode.lib import convert_line_endings, detect_mode
41 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
42 42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 43 from rhodecode.lib.base import BaseRepoController, render
44 44 from rhodecode.lib.utils import EmptyChangeset
45 45 import rhodecode.lib.helpers as h
46 46 from rhodecode.model.repo import RepoModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class FilesController(BaseRepoController):
52 52
53 53 @LoginRequired()
54 54 def __before__(self):
55 55 super(FilesController, self).__before__()
56 56 c.cut_off_limit = self.cut_off_limit
57 57
58 58 def __get_cs_or_redirect(self, rev, repo_name):
59 59 """
60 60 Safe way to get changeset if error occur it redirects to tip with
61 61 proper message
62 62
63 63 :param rev: revision to fetch
64 64 :param repo_name: repo name to redirect after
65 65 """
66 66
67 67 try:
68 68 return c.rhodecode_repo.get_changeset(rev)
69 69 except EmptyRepositoryError, e:
70 70 h.flash(_('There are no files yet'), category='warning')
71 71 redirect(h.url('summary_home', repo_name=repo_name))
72 72
73 73 except RepositoryError, e:
74 74 h.flash(str(e), category='warning')
75 75 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
76 76
77 77 def __get_filenode_or_redirect(self, repo_name, cs, path):
78 78 """
79 79 Returns file_node, if error occurs or given path is directory,
80 80 it'll redirect to top level path
81 81
82 82 :param repo_name: repo_name
83 83 :param cs: given changeset
84 84 :param path: path to lookup
85 85 """
86 86
87 87 try:
88 88 file_node = cs.get_node(path)
89 89 if file_node.is_dir():
90 90 raise RepositoryError('given path is a directory')
91 91 except RepositoryError, e:
92 92 h.flash(str(e), category='warning')
93 93 redirect(h.url('files_home', repo_name=repo_name,
94 94 revision=cs.raw_id))
95 95
96 96 return file_node
97 97
98 98 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
99 99 'repository.admin')
100 100 def index(self, repo_name, revision, f_path):
101 101 #reditect to given revision from form if given
102 102 post_revision = request.POST.get('at_rev', None)
103 103 if post_revision:
104 104 cs = self.__get_cs_or_redirect(post_revision, repo_name)
105 105 redirect(url('files_home', repo_name=c.repo_name,
106 106 revision=cs.raw_id, f_path=f_path))
107 107
108 108 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
109 109 c.branch = request.GET.get('branch', None)
110 110 c.f_path = f_path
111 111
112 112 cur_rev = c.changeset.revision
113 113
114 114 #prev link
115 115 try:
116 116 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
117 117 c.url_prev = url('files_home', repo_name=c.repo_name,
118 118 revision=prev_rev.raw_id, f_path=f_path)
119 119 if c.branch:
120 120 c.url_prev += '?branch=%s' % c.branch
121 121 except (ChangesetDoesNotExistError, VCSError):
122 122 c.url_prev = '#'
123 123
124 124 #next link
125 125 try:
126 126 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
127 127 c.url_next = url('files_home', repo_name=c.repo_name,
128 128 revision=next_rev.raw_id, f_path=f_path)
129 129 if c.branch:
130 130 c.url_next += '?branch=%s' % c.branch
131 131 except (ChangesetDoesNotExistError, VCSError):
132 132 c.url_next = '#'
133 133
134 134 #files or dirs
135 135 try:
136 136 c.files_list = c.changeset.get_node(f_path)
137 137
138 138 if c.files_list.is_file():
139 139 c.file_history = self._get_node_history(c.changeset, f_path)
140 140 else:
141 141 c.file_history = []
142 142 except RepositoryError, e:
143 143 h.flash(str(e), category='warning')
144 144 redirect(h.url('files_home', repo_name=repo_name,
145 145 revision=revision))
146 146
147 147 return render('files/files.html')
148 148
149 149 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
150 150 'repository.admin')
151 151 def rawfile(self, repo_name, revision, f_path):
152 152 cs = self.__get_cs_or_redirect(revision, repo_name)
153 153 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
154 154
155 155 response.content_disposition = 'attachment; filename=%s' % \
156 f_path.split(os.sep)[-1].encode('utf8', 'replace')
156 safe_str(f_path.split(os.sep)[-1])
157 157
158 158 response.content_type = file_node.mimetype
159 159 return file_node.content
160 160
161 161 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 162 'repository.admin')
163 163 def raw(self, repo_name, revision, f_path):
164 164 cs = self.__get_cs_or_redirect(revision, repo_name)
165 165 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
166 166
167 167 raw_mimetype_mapping = {
168 168 # map original mimetype to a mimetype used for "show as raw"
169 169 # you can also provide a content-disposition to override the
170 170 # default "attachment" disposition.
171 171 # orig_type: (new_type, new_dispo)
172 172
173 173 # show images inline:
174 174 'image/x-icon': ('image/x-icon', 'inline'),
175 175 'image/png': ('image/png', 'inline'),
176 176 'image/gif': ('image/gif', 'inline'),
177 177 'image/jpeg': ('image/jpeg', 'inline'),
178 178 'image/svg+xml': ('image/svg+xml', 'inline'),
179 179 }
180 180
181 181 mimetype = file_node.mimetype
182 182 try:
183 183 mimetype, dispo = raw_mimetype_mapping[mimetype]
184 184 except KeyError:
185 185 # we don't know anything special about this, handle it safely
186 186 if file_node.is_binary:
187 187 # do same as download raw for binary files
188 188 mimetype, dispo = 'application/octet-stream', 'attachment'
189 189 else:
190 190 # do not just use the original mimetype, but force text/plain,
191 191 # otherwise it would serve text/html and that might be unsafe.
192 192 # Note: underlying vcs library fakes text/plain mimetype if the
193 193 # mimetype can not be determined and it thinks it is not
194 194 # binary.This might lead to erroneous text display in some
195 195 # cases, but helps in other cases, like with text files
196 196 # without extension.
197 197 mimetype, dispo = 'text/plain', 'inline'
198 198
199 199 if dispo == 'attachment':
200 200 dispo = 'attachment; filename=%s' % \
201 f_path.split(os.sep)[-1].encode('utf8', 'replace')
201 safe_str(f_path.split(os.sep)[-1])
202 202
203 203 response.content_disposition = dispo
204 204 response.content_type = mimetype
205 205 return file_node.content
206 206
207 207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
208 208 'repository.admin')
209 209 def annotate(self, repo_name, revision, f_path):
210 210 c.cs = self.__get_cs_or_redirect(revision, repo_name)
211 211 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
212 212
213 213 c.file_history = self._get_node_history(c.cs, f_path)
214 214 c.f_path = f_path
215 215 return render('files/files_annotate.html')
216 216
217 217 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
218 218 def edit(self, repo_name, revision, f_path):
219 219 r_post = request.POST
220 220
221 221 c.cs = self.__get_cs_or_redirect(revision, repo_name)
222 222 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
223 223
224 224 if c.file.is_binary:
225 225 return redirect(url('files_home', repo_name=c.repo_name,
226 226 revision=c.cs.raw_id, f_path=f_path))
227 227
228 228 c.file_history = self._get_node_history(c.cs, f_path)
229 229 c.f_path = f_path
230 230
231 231 if r_post:
232 232
233 233 old_content = c.file.content
234 234 sl = old_content.splitlines(1)
235 235 first_line = sl[0] if sl else ''
236 236 # modes: 0 - Unix, 1 - Mac, 2 - DOS
237 237 mode = detect_mode(first_line, 0)
238 238 content = convert_line_endings(r_post.get('content'), mode)
239 239
240 240 message = r_post.get('message') or (_('Edited %s via RhodeCode')
241 241 % (f_path))
242 242 author = self.rhodecode_user.full_contact
243 243
244 244 if content == old_content:
245 245 h.flash(_('No changes'),
246 246 category='warning')
247 247 return redirect(url('changeset_home', repo_name=c.repo_name,
248 248 revision='tip'))
249 249
250 250 try:
251 251 self.scm_model.commit_change(repo=c.rhodecode_repo,
252 252 repo_name=repo_name, cs=c.cs,
253 253 user=self.rhodecode_user,
254 254 author=author, message=message,
255 255 content=content, f_path=f_path)
256 256 h.flash(_('Successfully committed to %s' % f_path),
257 257 category='success')
258 258
259 259 except Exception:
260 260 log.error(traceback.format_exc())
261 261 h.flash(_('Error occurred during commit'), category='error')
262 262 return redirect(url('changeset_home',
263 263 repo_name=c.repo_name, revision='tip'))
264 264
265 265 return render('files/files_edit.html')
266 266
267 267 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
268 268 'repository.admin')
269 269 def archivefile(self, repo_name, fname):
270 270
271 271 fileformat = None
272 272 revision = None
273 273 ext = None
274 274
275 275 for a_type, ext_data in ARCHIVE_SPECS.items():
276 276 archive_spec = fname.split(ext_data[1])
277 277 if len(archive_spec) == 2 and archive_spec[1] == '':
278 278 fileformat = a_type or ext_data[1]
279 279 revision = archive_spec[0]
280 280 ext = ext_data[1]
281 281
282 282 try:
283 283 dbrepo = RepoModel().get_by_repo_name(repo_name)
284 284 if dbrepo.enable_downloads is False:
285 285 return _('downloads disabled')
286 286
287 287 cs = c.rhodecode_repo.get_changeset(revision)
288 288 content_type = ARCHIVE_SPECS[fileformat][0]
289 289 except ChangesetDoesNotExistError:
290 290 return _('Unknown revision %s') % revision
291 291 except EmptyRepositoryError:
292 292 return _('Empty repository')
293 293 except (ImproperArchiveTypeError, KeyError):
294 294 return _('Unknown archive type')
295 295
296 296 response.content_type = content_type
297 297 response.content_disposition = 'attachment; filename=%s-%s%s' \
298 298 % (repo_name, revision, ext)
299 299
300 300 import tempfile
301 301 archive = tempfile.mkstemp()[1]
302 302 t = open(archive, 'wb')
303 303 cs.fill_archive(stream=t, kind=fileformat)
304 304
305 305 def get_chunked_archive(archive):
306 306 stream = open(archive, 'rb')
307 307 while True:
308 308 data = stream.read(4096)
309 309 if not data:
310 310 os.remove(archive)
311 311 break
312 312 yield data
313 313
314 314 return get_chunked_archive(archive)
315 315
316 316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 317 'repository.admin')
318 318 def diff(self, repo_name, f_path):
319 319 diff1 = request.GET.get('diff1')
320 320 diff2 = request.GET.get('diff2')
321 321 c.action = request.GET.get('diff')
322 322 c.no_changes = diff1 == diff2
323 323 c.f_path = f_path
324 324 c.big_diff = False
325 325
326 326 try:
327 327 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
328 328 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
329 329 node1 = c.changeset_1.get_node(f_path)
330 330 else:
331 331 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
332 332 node1 = FileNode('.', '', changeset=c.changeset_1)
333 333
334 334 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
335 335 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
336 336 node2 = c.changeset_2.get_node(f_path)
337 337 else:
338 338 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
339 339 node2 = FileNode('.', '', changeset=c.changeset_2)
340 340 except RepositoryError:
341 341 return redirect(url('files_home',
342 342 repo_name=c.repo_name, f_path=f_path))
343 343
344 344 if c.action == 'download':
345 345 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
346 346 format='gitdiff')
347 347
348 348 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
349 349 response.content_type = 'text/plain'
350 350 response.content_disposition = 'attachment; filename=%s' \
351 351 % diff_name
352 352 return diff.raw_diff()
353 353
354 354 elif c.action == 'raw':
355 355 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
356 356 format='gitdiff')
357 357 response.content_type = 'text/plain'
358 358 return diff.raw_diff()
359 359
360 360 elif c.action == 'diff':
361 361 if node1.is_binary or node2.is_binary:
362 362 c.cur_diff = _('Binary file')
363 363 elif node1.size > self.cut_off_limit or \
364 364 node2.size > self.cut_off_limit:
365 365 c.cur_diff = ''
366 366 c.big_diff = True
367 367 else:
368 368 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
369 369 format='gitdiff')
370 370 c.cur_diff = diff.as_html()
371 371 else:
372 372
373 373 #default option
374 374 if node1.is_binary or node2.is_binary:
375 375 c.cur_diff = _('Binary file')
376 376 elif node1.size > self.cut_off_limit or \
377 377 node2.size > self.cut_off_limit:
378 378 c.cur_diff = ''
379 379 c.big_diff = True
380 380
381 381 else:
382 382 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
383 383 format='gitdiff')
384 384 c.cur_diff = diff.as_html()
385 385
386 386 if not c.cur_diff and not c.big_diff:
387 387 c.no_changes = True
388 388 return render('files/file_diff.html')
389 389
390 390 def _get_node_history(self, cs, f_path):
391 391 changesets = cs.get_file_history(f_path)
392 392 hist_l = []
393 393
394 394 changesets_group = ([], _("Changesets"))
395 395 branches_group = ([], _("Branches"))
396 396 tags_group = ([], _("Tags"))
397 397
398 398 for chs in changesets:
399 399 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
400 400 changesets_group[0].append((chs.raw_id, n_desc,))
401 401
402 402 hist_l.append(changesets_group)
403 403
404 404 for name, chs in c.rhodecode_repo.branches.items():
405 405 #chs = chs.split(':')[-1]
406 406 branches_group[0].append((chs, name),)
407 407 hist_l.append(branches_group)
408 408
409 409 for name, chs in c.rhodecode_repo.tags.items():
410 410 #chs = chs.split(':')[-1]
411 411 tags_group[0].append((chs, name),)
412 412 hist_l.append(tags_group)
413 413
414 414 return hist_l
@@ -1,322 +1,344 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.__init__
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26
27 27 try:
28 28 import json
29 29 except ImportError:
30 30 #python 2.5 compatibility
31 31 import simplejson as json
32 32
33 33
34 34 def __get_lem():
35 35 from pygments import lexers
36 36 from string import lower
37 37 from collections import defaultdict
38 38
39 39 d = defaultdict(lambda: [])
40 40
41 41 def __clean(s):
42 42 s = s.lstrip('*')
43 43 s = s.lstrip('.')
44 44
45 45 if s.find('[') != -1:
46 46 exts = []
47 47 start, stop = s.find('['), s.find(']')
48 48
49 49 for suffix in s[start + 1:stop]:
50 50 exts.append(s[:s.find('[')] + suffix)
51 51 return map(lower, exts)
52 52 else:
53 53 return map(lower, [s])
54 54
55 55 for lx, t in sorted(lexers.LEXERS.items()):
56 56 m = map(__clean, t[-2])
57 57 if m:
58 58 m = reduce(lambda x, y: x + y, m)
59 59 for ext in m:
60 60 desc = lx.replace('Lexer', '')
61 61 d[ext].append(desc)
62 62
63 63 return dict(d)
64 64
65 65 # language map is also used by whoosh indexer, which for those specified
66 66 # extensions will index it's content
67 67 LANGUAGES_EXTENSIONS_MAP = __get_lem()
68 68
69 69 # Additional mappings that are not present in the pygments lexers
70 70 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
71 71 ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}
72 72
73 73 LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
74 74
75 75
76 76 def str2bool(_str):
77 77 """
78 78 returs True/False value from given string, it tries to translate the
79 79 string into boolean
80 80
81 81 :param _str: string value to translate into boolean
82 82 :rtype: boolean
83 83 :returns: boolean from given string
84 84 """
85 85 if _str is None:
86 86 return False
87 87 if _str in (True, False):
88 88 return _str
89 89 _str = str(_str).strip().lower()
90 90 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
91 91
92 92
93 93 def convert_line_endings(line, mode):
94 94 """
95 95 Converts a given line "line end" accordingly to given mode
96 96
97 97 Available modes are::
98 98 0 - Unix
99 99 1 - Mac
100 100 2 - DOS
101 101
102 102 :param line: given line to convert
103 103 :param mode: mode to convert to
104 104 :rtype: str
105 105 :return: converted line according to mode
106 106 """
107 107 from string import replace
108 108
109 109 if mode == 0:
110 110 line = replace(line, '\r\n', '\n')
111 111 line = replace(line, '\r', '\n')
112 112 elif mode == 1:
113 113 line = replace(line, '\r\n', '\r')
114 114 line = replace(line, '\n', '\r')
115 115 elif mode == 2:
116 116 import re
117 117 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
118 118 return line
119 119
120 120
121 121 def detect_mode(line, default):
122 122 """
123 123 Detects line break for given line, if line break couldn't be found
124 124 given default value is returned
125 125
126 126 :param line: str line
127 127 :param default: default
128 128 :rtype: int
129 129 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
130 130 """
131 131 if line.endswith('\r\n'):
132 132 return 2
133 133 elif line.endswith('\n'):
134 134 return 0
135 135 elif line.endswith('\r'):
136 136 return 1
137 137 else:
138 138 return default
139 139
140 140
141 141 def generate_api_key(username, salt=None):
142 142 """
143 143 Generates unique API key for given username,if salt is not given
144 144 it'll be generated from some random string
145 145
146 146 :param username: username as string
147 147 :param salt: salt to hash generate KEY
148 148 :rtype: str
149 149 :returns: sha1 hash from username+salt
150 150 """
151 151 from tempfile import _RandomNameSequence
152 152 import hashlib
153 153
154 154 if salt is None:
155 155 salt = _RandomNameSequence().next()
156 156
157 157 return hashlib.sha1(username + salt).hexdigest()
158 158
159 159
160 160 def safe_unicode(_str, from_encoding='utf8'):
161 161 """
162 162 safe unicode function. In case of UnicodeDecode error we try to return
163 unicode with errors replace
163 unicode with errors replaceed
164 164
165 165 :param _str: string to decode
166 166 :rtype: unicode
167 167 :returns: unicode object
168 168 """
169 169
170 170 if isinstance(_str, unicode):
171 171 return _str
172 172
173 173 try:
174 174 u_str = unicode(_str, from_encoding)
175 175 except UnicodeDecodeError:
176 176 u_str = unicode(_str, from_encoding, 'replace')
177 177
178 178 return u_str
179 179
180 180
181 def safe_str(_unicode, to_encoding='utf8'):
182 """
183 safe str function. In case of UnicodeEncode error we try to return
184 str with errors replaceed
185
186 :param _unicode: unicode to encode
187 :rtype: str
188 :returns: str object
189 """
190
191 if isinstance(_unicode, str):
192 return _unicode
193
194 try:
195 safe_str = str(_unicode)
196 except UnicodeEncodeError:
197 safe_str = _unicode.encode(to_encoding, 'replace')
198
199 return safe_str
200
201
202
181 203 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
182 204 """
183 205 Custom engine_from_config functions that makes sure we use NullPool for
184 206 file based sqlite databases. This prevents errors on sqlite. This only
185 207 applies to sqlalchemy versions < 0.7.0
186 208
187 209 """
188 210 import sqlalchemy
189 211 from sqlalchemy import engine_from_config as efc
190 212 import logging
191 213
192 214 if int(sqlalchemy.__version__.split('.')[1]) < 7:
193 215
194 216 # This solution should work for sqlalchemy < 0.7.0, and should use
195 217 # proxy=TimerProxy() for execution time profiling
196 218
197 219 from sqlalchemy.pool import NullPool
198 220 url = configuration[prefix + 'url']
199 221
200 222 if url.startswith('sqlite'):
201 223 kwargs.update({'poolclass': NullPool})
202 224 return efc(configuration, prefix, **kwargs)
203 225 else:
204 226 import time
205 227 from sqlalchemy import event
206 228 from sqlalchemy.engine import Engine
207 229
208 230 log = logging.getLogger('sqlalchemy.engine')
209 231 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
210 232 engine = efc(configuration, prefix, **kwargs)
211 233
212 234 def color_sql(sql):
213 235 COLOR_SEQ = "\033[1;%dm"
214 236 COLOR_SQL = YELLOW
215 237 normal = '\x1b[0m'
216 238 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
217 239
218 240 if configuration['debug']:
219 241 #attach events only for debug configuration
220 242
221 243 def before_cursor_execute(conn, cursor, statement,
222 244 parameters, context, executemany):
223 245 context._query_start_time = time.time()
224 246 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
225 247
226 248
227 249 def after_cursor_execute(conn, cursor, statement,
228 250 parameters, context, executemany):
229 251 total = time.time() - context._query_start_time
230 252 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
231 253
232 254 event.listen(engine, "before_cursor_execute",
233 255 before_cursor_execute)
234 256 event.listen(engine, "after_cursor_execute",
235 257 after_cursor_execute)
236 258
237 259 return engine
238 260
239 261
240 262 def age(curdate):
241 263 """
242 264 turns a datetime into an age string.
243 265
244 266 :param curdate: datetime object
245 267 :rtype: unicode
246 268 :returns: unicode words describing age
247 269 """
248 270
249 271 from datetime import datetime
250 272 from webhelpers.date import time_ago_in_words
251 273
252 274 _ = lambda s:s
253 275
254 276 if not curdate:
255 277 return ''
256 278
257 279 agescales = [(_(u"year"), 3600 * 24 * 365),
258 280 (_(u"month"), 3600 * 24 * 30),
259 281 (_(u"day"), 3600 * 24),
260 282 (_(u"hour"), 3600),
261 283 (_(u"minute"), 60),
262 284 (_(u"second"), 1), ]
263 285
264 286 age = datetime.now() - curdate
265 287 age_seconds = (age.days * agescales[2][1]) + age.seconds
266 288 pos = 1
267 289 for scale in agescales:
268 290 if scale[1] <= age_seconds:
269 291 if pos == 6:pos = 5
270 292 return '%s %s' % (time_ago_in_words(curdate,
271 293 agescales[pos][0]), _('ago'))
272 294 pos += 1
273 295
274 296 return _(u'just now')
275 297
276 298
277 299 def uri_filter(uri):
278 300 """
279 301 Removes user:password from given url string
280 302
281 303 :param uri:
282 304 :rtype: unicode
283 305 :returns: filtered list of strings
284 306 """
285 307 if not uri:
286 308 return ''
287 309
288 310 proto = ''
289 311
290 312 for pat in ('https://', 'http://'):
291 313 if uri.startswith(pat):
292 314 uri = uri[len(pat):]
293 315 proto = pat
294 316 break
295 317
296 318 # remove passwords and username
297 319 uri = uri[uri.find('@') + 1:]
298 320
299 321 # get the port
300 322 cred_pos = uri.find(':')
301 323 if cred_pos == -1:
302 324 host, port = uri, None
303 325 else:
304 326 host, port = uri[:cred_pos], uri[cred_pos + 1:]
305 327
306 328 return filter(None, [proto, host, port])
307 329
308 330
309 331 def credentials_filter(uri):
310 332 """
311 333 Returns a url with removed credentials
312 334
313 335 :param uri:
314 336 """
315 337
316 338 uri = uri_filter(uri)
317 339 #check if we have port
318 340 if len(uri) > 2 and uri[2]:
319 341 uri[2] = ':' + uri[2]
320 342
321 343 return ''.join(uri)
322 344
@@ -1,377 +1,374 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.tasks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode task modules, containing all task that suppose to be run
7 7 by celery daemon
8 8
9 9 :created_on: Oct 6, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 from celery.decorators import task
27 27
28 28 import os
29 29 import traceback
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32
33 33 from time import mktime
34 34 from operator import itemgetter
35 35 from string import lower
36 36
37 37 from pylons import config
38 38 from pylons.i18n.translation import _
39 39
40 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP
40 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
41 41 from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
42 42 __get_lockkey, LockHeld, DaemonLock
43 43 from rhodecode.lib.helpers import person
44 44 from rhodecode.lib.smtp_mailer import SmtpMailer
45 45 from rhodecode.lib.utils import add_cache
46 46 from rhodecode.lib.odict import OrderedDict
47 47 from rhodecode.model import init_model
48 48 from rhodecode.model import meta
49 49 from rhodecode.model.db import RhodeCodeUi, Statistics, Repository
50 50
51 51 from vcs.backends import get_repo
52 52
53 53 from sqlalchemy import engine_from_config
54 54
55 55 add_cache(config)
56 56
57 57 try:
58 58 import json
59 59 except ImportError:
60 60 #python 2.5 compatibility
61 61 import simplejson as json
62 62
63 63 __all__ = ['whoosh_index', 'get_commits_stats',
64 64 'reset_user_password', 'send_email']
65 65
66 66 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
67 67
68 68
69
70 69 def get_session():
71 70 if CELERY_ON:
72 71 engine = engine_from_config(config, 'sqlalchemy.db1.')
73 72 init_model(engine)
74 73 sa = meta.Session()
75 74 return sa
76 75
77 76
78 77 def get_repos_path():
79 78 sa = get_session()
80 79 q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
81 80 return q.ui_value
82 81
83 82
84 83 @task(ignore_result=True)
85 84 @locked_task
86 85 def whoosh_index(repo_location, full_index):
87 86 #log = whoosh_index.get_logger()
88 87 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
89 88 index_location = config['index_dir']
90 89 WhooshIndexingDaemon(index_location=index_location,
91 90 repo_location=repo_location, sa=get_session())\
92 91 .run(full_index=full_index)
93 92
94 93
95 94 @task(ignore_result=True)
96 95 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
97 96 try:
98 97 log = get_commits_stats.get_logger()
99 98 except:
100 99 log = logging.getLogger(__name__)
101 100
102 101 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
103 102 ts_max_y)
104 103 lockkey_path = dn(dn(dn(dn(os.path.abspath(__file__)))))
105 104 log.info('running task with lockkey %s', lockkey)
106 105 try:
107 106 lock = l = DaemonLock(jn(lockkey_path, lockkey))
108 107
109 108 #for js data compatibilty cleans the key for person from '
110 109 akc = lambda k: person(k).replace('"', "")
111 110
112 111 co_day_auth_aggr = {}
113 112 commits_by_day_aggregate = {}
114 113 repos_path = get_repos_path()
115 p = os.path.join(repos_path, repo_name)
116 repo = get_repo(p)
114 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
117 115 repo_size = len(repo.revisions)
118 116 #return if repo have no revisions
119 117 if repo_size < 1:
120 118 lock.release()
121 119 return True
122 120
123 121 skip_date_limit = True
124 122 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
125 123 last_rev = 0
126 124 last_cs = None
127 125 timegetter = itemgetter('time')
128 126
129 127 sa = get_session()
130 128
131 129 dbrepo = sa.query(Repository)\
132 130 .filter(Repository.repo_name == repo_name).scalar()
133 131 cur_stats = sa.query(Statistics)\
134 132 .filter(Statistics.repository == dbrepo).scalar()
135 133
136 134 if cur_stats is not None:
137 135 last_rev = cur_stats.stat_on_revision
138 136
139 137 if last_rev == repo.get_changeset().revision and repo_size > 1:
140 138 #pass silently without any work if we're not on first revision or
141 139 #current state of parsing revision(from db marker) is the
142 140 #last revision
143 141 lock.release()
144 142 return True
145 143
146 144 if cur_stats:
147 145 commits_by_day_aggregate = OrderedDict(json.loads(
148 146 cur_stats.commit_activity_combined))
149 147 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
150 148
151 149 log.debug('starting parsing %s', parse_limit)
152 150 lmktime = mktime
153 151
154 152 last_rev = last_rev + 1 if last_rev > 0 else last_rev
155 153
156 154 for cs in repo[last_rev:last_rev + parse_limit]:
157 155 last_cs = cs # remember last parsed changeset
158 156 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
159 157 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
160 158
161 159 if akc(cs.author) in co_day_auth_aggr:
162 160 try:
163 161 l = [timegetter(x) for x in
164 162 co_day_auth_aggr[akc(cs.author)]['data']]
165 163 time_pos = l.index(k)
166 164 except ValueError:
167 165 time_pos = False
168 166
169 167 if time_pos >= 0 and time_pos is not False:
170 168
171 169 datadict = \
172 170 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
173 171
174 172 datadict["commits"] += 1
175 173 datadict["added"] += len(cs.added)
176 174 datadict["changed"] += len(cs.changed)
177 175 datadict["removed"] += len(cs.removed)
178 176
179 177 else:
180 178 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
181 179
182 180 datadict = {"time": k,
183 181 "commits": 1,
184 182 "added": len(cs.added),
185 183 "changed": len(cs.changed),
186 184 "removed": len(cs.removed),
187 185 }
188 186 co_day_auth_aggr[akc(cs.author)]['data']\
189 187 .append(datadict)
190 188
191 189 else:
192 190 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
193 191 co_day_auth_aggr[akc(cs.author)] = {
194 192 "label": akc(cs.author),
195 193 "data": [{"time":k,
196 194 "commits":1,
197 195 "added":len(cs.added),
198 196 "changed":len(cs.changed),
199 197 "removed":len(cs.removed),
200 198 }],
201 199 "schema": ["commits"],
202 200 }
203 201
204 202 #gather all data by day
205 203 if k in commits_by_day_aggregate:
206 204 commits_by_day_aggregate[k] += 1
207 205 else:
208 206 commits_by_day_aggregate[k] = 1
209 207
210 208 overview_data = sorted(commits_by_day_aggregate.items(),
211 209 key=itemgetter(0))
212 210
213 211 if not co_day_auth_aggr:
214 212 co_day_auth_aggr[akc(repo.contact)] = {
215 213 "label": akc(repo.contact),
216 214 "data": [0, 1],
217 215 "schema": ["commits"],
218 216 }
219 217
220 218 stats = cur_stats if cur_stats else Statistics()
221 219 stats.commit_activity = json.dumps(co_day_auth_aggr)
222 220 stats.commit_activity_combined = json.dumps(overview_data)
223 221
224 222 log.debug('last revison %s', last_rev)
225 223 leftovers = len(repo.revisions[last_rev:])
226 224 log.debug('revisions to parse %s', leftovers)
227 225
228 226 if last_rev == 0 or leftovers < parse_limit:
229 227 log.debug('getting code trending stats')
230 228 stats.languages = json.dumps(__get_codes_stats(repo_name))
231 229
232 230 try:
233 231 stats.repository = dbrepo
234 232 stats.stat_on_revision = last_cs.revision if last_cs else 0
235 233 sa.add(stats)
236 234 sa.commit()
237 235 except:
238 236 log.error(traceback.format_exc())
239 237 sa.rollback()
240 238 lock.release()
241 239 return False
242 240
243 241 #final release
244 242 lock.release()
245 243
246 244 #execute another task if celery is enabled
247 245 if len(repo.revisions) > 1 and CELERY_ON:
248 246 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y)
249 247 return True
250 248 except LockHeld:
251 249 log.info('LockHeld')
252 250 return 'Task with key %s already running' % lockkey
253 251
254 252
255 253 @task(ignore_result=True)
256 254 def reset_user_password(user_email):
257 255 try:
258 256 log = reset_user_password.get_logger()
259 257 except:
260 258 log = logging.getLogger(__name__)
261 259
262 260 from rhodecode.lib import auth
263 261 from rhodecode.model.db import User
264 262
265 263 try:
266 264 try:
267 265 sa = get_session()
268 266 user = sa.query(User).filter(User.email == user_email).scalar()
269 267 new_passwd = auth.PasswordGenerator().gen_password(8,
270 268 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
271 269 if user:
272 270 user.password = auth.get_crypt_password(new_passwd)
273 271 user.api_key = auth.generate_api_key(user.username)
274 272 sa.add(user)
275 273 sa.commit()
276 274 log.info('change password for %s', user_email)
277 275 if new_passwd is None:
278 276 raise Exception('unable to generate new password')
279 277
280 278 except:
281 279 log.error(traceback.format_exc())
282 280 sa.rollback()
283 281
284 282 run_task(send_email, user_email,
285 283 "Your new rhodecode password",
286 284 'Your new rhodecode password:%s' % (new_passwd))
287 285 log.info('send new password mail to %s', user_email)
288 286
289 287 except:
290 288 log.error('Failed to update user password')
291 289 log.error(traceback.format_exc())
292 290
293 291 return True
294 292
295 293
296 294 @task(ignore_result=True)
297 295 def send_email(recipients, subject, body):
298 296 """
299 297 Sends an email with defined parameters from the .ini files.
300 298
301 299 :param recipients: list of recipients, it this is empty the defined email
302 300 address from field 'email_to' is used instead
303 301 :param subject: subject of the mail
304 302 :param body: body of the mail
305 303 """
306 304 try:
307 305 log = send_email.get_logger()
308 306 except:
309 307 log = logging.getLogger(__name__)
310 308
311 309 email_config = config
312 310
313 311 if not recipients:
314 312 recipients = [email_config.get('email_to')]
315 313
316 314 mail_from = email_config.get('app_email_from')
317 315 user = email_config.get('smtp_username')
318 316 passwd = email_config.get('smtp_password')
319 317 mail_server = email_config.get('smtp_server')
320 318 mail_port = email_config.get('smtp_port')
321 319 tls = str2bool(email_config.get('smtp_use_tls'))
322 320 ssl = str2bool(email_config.get('smtp_use_ssl'))
323 321 debug = str2bool(config.get('debug'))
324 322
325 323 try:
326 324 m = SmtpMailer(mail_from, user, passwd, mail_server,
327 325 mail_port, ssl, tls, debug=debug)
328 326 m.send(recipients, subject, body)
329 327 except:
330 328 log.error('Mail sending failed')
331 329 log.error(traceback.format_exc())
332 330 return False
333 331 return True
334 332
335 333
336 334 @task(ignore_result=True)
337 335 def create_repo_fork(form_data, cur_user):
338 336 from rhodecode.model.repo import RepoModel
339 337 from vcs import get_backend
340 338
341 339 try:
342 340 log = create_repo_fork.get_logger()
343 341 except:
344 342 log = logging.getLogger(__name__)
345 343
346 344 repo_model = RepoModel(get_session())
347 345 repo_model.create(form_data, cur_user, just_db=True, fork=True)
348 346 repo_name = form_data['repo_name']
349 347 repos_path = get_repos_path()
350 348 repo_path = os.path.join(repos_path, repo_name)
351 349 repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
352 350 alias = form_data['repo_type']
353 351
354 352 log.info('creating repo fork %s as %s', repo_name, repo_path)
355 353 backend = get_backend(alias)
356 354 backend(str(repo_fork_path), create=True, src_url=str(repo_path))
357 355
358 356
359 357 def __get_codes_stats(repo_name):
360 358 repos_path = get_repos_path()
361 p = os.path.join(repos_path, repo_name)
362 repo = get_repo(p)
359 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
363 360 tip = repo.get_changeset()
364 361 code_stats = {}
365 362
366 363 def aggregate(cs):
367 364 for f in cs[2]:
368 365 ext = lower(f.extension)
369 366 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
370 367 if ext in code_stats:
371 368 code_stats[ext] += 1
372 369 else:
373 370 code_stats[ext] = 1
374 371
375 372 map(aggregate, tip.walk('/'))
376 373
377 374 return code_stats or {}
@@ -1,694 +1,694 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10
11 11 from datetime import datetime
12 12 from pygments.formatters import HtmlFormatter
13 13 from pygments import highlight as code_highlight
14 14 from pylons import url, request, config
15 15 from pylons.i18n.translation import _, ungettext
16 16
17 17 from webhelpers.html import literal, HTML, escape
18 18 from webhelpers.html.tools import *
19 19 from webhelpers.html.builder import make_tag
20 20 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
21 21 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
22 22 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
23 23 password, textarea, title, ul, xml_declaration, radio
24 24 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
25 25 mail_to, strip_links, strip_tags, tag_re
26 26 from webhelpers.number import format_byte_size, format_bit_size
27 27 from webhelpers.pylonslib import Flash as _Flash
28 28 from webhelpers.pylonslib.secure_form import secure_form
29 29 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
30 30 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
31 31 replace_whitespace, urlify, truncate, wrap_paragraphs
32 32 from webhelpers.date import time_ago_in_words
33 33 from webhelpers.paginate import Page
34 34 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
35 35 convert_boolean_attrs, NotGiven
36 36
37 37 from vcs.utils.annotate import annotate_highlight
38 38 from rhodecode.lib.utils import repo_name_slug
39 from rhodecode.lib import str2bool, safe_unicode
39 from rhodecode.lib import str2bool, safe_unicode, safe_str
40 40
41 41 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
42 42 """
43 43 Reset button
44 44 """
45 45 _set_input_attrs(attrs, type, name, value)
46 46 _set_id_attr(attrs, id, name)
47 47 convert_boolean_attrs(attrs, ["disabled"])
48 48 return HTML.input(**attrs)
49 49
50 50 reset = _reset
51 51
52 52
53 53 def get_token():
54 54 """Return the current authentication token, creating one if one doesn't
55 55 already exist.
56 56 """
57 57 token_key = "_authentication_token"
58 58 from pylons import session
59 59 if not token_key in session:
60 60 try:
61 61 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
62 62 except AttributeError: # Python < 2.4
63 63 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
64 64 session[token_key] = token
65 65 if hasattr(session, 'save'):
66 66 session.save()
67 67 return session[token_key]
68 68
69 69 class _GetError(object):
70 70 """Get error from form_errors, and represent it as span wrapped error
71 71 message
72 72
73 73 :param field_name: field to fetch errors for
74 74 :param form_errors: form errors dict
75 75 """
76 76
77 77 def __call__(self, field_name, form_errors):
78 78 tmpl = """<span class="error_msg">%s</span>"""
79 79 if form_errors and form_errors.has_key(field_name):
80 80 return literal(tmpl % form_errors.get(field_name))
81 81
82 82 get_error = _GetError()
83 83
84 84 class _ToolTip(object):
85 85
86 86 def __call__(self, tooltip_title, trim_at=50):
87 87 """Special function just to wrap our text into nice formatted
88 88 autowrapped text
89 89
90 90 :param tooltip_title:
91 91 """
92 92
93 93 return escape(tooltip_title)
94 94
95 95 def activate(self):
96 96 """Adds tooltip mechanism to the given Html all tooltips have to have
97 97 set class `tooltip` and set attribute `tooltip_title`.
98 98 Then a tooltip will be generated based on that. All with yui js tooltip
99 99 """
100 100
101 101 js = '''
102 102 YAHOO.util.Event.onDOMReady(function(){
103 103 function toolTipsId(){
104 104 var ids = [];
105 105 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
106 106
107 107 for (var i = 0; i < tts.length; i++) {
108 108 //if element doesn't not have and id autogenerate one for tooltip
109 109
110 110 if (!tts[i].id){
111 111 tts[i].id='tt'+i*100;
112 112 }
113 113 ids.push(tts[i].id);
114 114 }
115 115 return ids
116 116 };
117 117 var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
118 118 context: [[toolTipsId()],"tl","bl",null,[0,5]],
119 119 monitorresize:false,
120 120 xyoffset :[0,0],
121 121 autodismissdelay:300000,
122 122 hidedelay:5,
123 123 showdelay:20,
124 124 });
125 125
126 126 });
127 127 '''
128 128 return literal(js)
129 129
130 130 tooltip = _ToolTip()
131 131
132 132 class _FilesBreadCrumbs(object):
133 133
134 134 def __call__(self, repo_name, rev, paths):
135 135 if isinstance(paths, str):
136 136 paths = safe_unicode(paths)
137 137 url_l = [link_to(repo_name, url('files_home',
138 138 repo_name=repo_name,
139 139 revision=rev, f_path=''))]
140 140 paths_l = paths.split('/')
141 141 for cnt, p in enumerate(paths_l):
142 142 if p != '':
143 143 url_l.append(link_to(p, url('files_home',
144 144 repo_name=repo_name,
145 145 revision=rev,
146 146 f_path='/'.join(paths_l[:cnt + 1]))))
147 147
148 148 return literal('/'.join(url_l))
149 149
150 150 files_breadcrumbs = _FilesBreadCrumbs()
151 151
152 152 class CodeHtmlFormatter(HtmlFormatter):
153 153 """My code Html Formatter for source codes
154 154 """
155 155
156 156 def wrap(self, source, outfile):
157 157 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
158 158
159 159 def _wrap_code(self, source):
160 160 for cnt, it in enumerate(source):
161 161 i, t = it
162 162 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
163 163 yield i, t
164 164
165 165 def _wrap_tablelinenos(self, inner):
166 166 dummyoutfile = StringIO.StringIO()
167 167 lncount = 0
168 168 for t, line in inner:
169 169 if t:
170 170 lncount += 1
171 171 dummyoutfile.write(line)
172 172
173 173 fl = self.linenostart
174 174 mw = len(str(lncount + fl - 1))
175 175 sp = self.linenospecial
176 176 st = self.linenostep
177 177 la = self.lineanchors
178 178 aln = self.anchorlinenos
179 179 nocls = self.noclasses
180 180 if sp:
181 181 lines = []
182 182
183 183 for i in range(fl, fl + lncount):
184 184 if i % st == 0:
185 185 if i % sp == 0:
186 186 if aln:
187 187 lines.append('<a href="#%s%d" class="special">%*d</a>' %
188 188 (la, i, mw, i))
189 189 else:
190 190 lines.append('<span class="special">%*d</span>' % (mw, i))
191 191 else:
192 192 if aln:
193 193 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
194 194 else:
195 195 lines.append('%*d' % (mw, i))
196 196 else:
197 197 lines.append('')
198 198 ls = '\n'.join(lines)
199 199 else:
200 200 lines = []
201 201 for i in range(fl, fl + lncount):
202 202 if i % st == 0:
203 203 if aln:
204 204 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
205 205 else:
206 206 lines.append('%*d' % (mw, i))
207 207 else:
208 208 lines.append('')
209 209 ls = '\n'.join(lines)
210 210
211 211 # in case you wonder about the seemingly redundant <div> here: since the
212 212 # content in the other cell also is wrapped in a div, some browsers in
213 213 # some configurations seem to mess up the formatting...
214 214 if nocls:
215 215 yield 0, ('<table class="%stable">' % self.cssclass +
216 216 '<tr><td><div class="linenodiv" '
217 217 'style="background-color: #f0f0f0; padding-right: 10px">'
218 218 '<pre style="line-height: 125%">' +
219 219 ls + '</pre></div></td><td id="hlcode" class="code">')
220 220 else:
221 221 yield 0, ('<table class="%stable">' % self.cssclass +
222 222 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
223 223 ls + '</pre></div></td><td id="hlcode" class="code">')
224 224 yield 0, dummyoutfile.getvalue()
225 225 yield 0, '</td></tr></table>'
226 226
227 227
228 228 def pygmentize(filenode, **kwargs):
229 229 """pygmentize function using pygments
230 230
231 231 :param filenode:
232 232 """
233 233
234 234 return literal(code_highlight(filenode.content,
235 235 filenode.lexer, CodeHtmlFormatter(**kwargs)))
236 236
237 237 def pygmentize_annotation(repo_name, filenode, **kwargs):
238 238 """pygmentize function for annotation
239 239
240 240 :param filenode:
241 241 """
242 242
243 243 color_dict = {}
244 244 def gen_color(n=10000):
245 245 """generator for getting n of evenly distributed colors using
246 246 hsv color and golden ratio. It always return same order of colors
247 247
248 248 :returns: RGB tuple
249 249 """
250 250 import colorsys
251 251 golden_ratio = 0.618033988749895
252 252 h = 0.22717784590367374
253 253
254 254 for _ in xrange(n):
255 255 h += golden_ratio
256 256 h %= 1
257 257 HSV_tuple = [h, 0.95, 0.95]
258 258 RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple)
259 259 yield map(lambda x:str(int(x * 256)), RGB_tuple)
260 260
261 261 cgenerator = gen_color()
262 262
263 263 def get_color_string(cs):
264 264 if color_dict.has_key(cs):
265 265 col = color_dict[cs]
266 266 else:
267 267 col = color_dict[cs] = cgenerator.next()
268 268 return "color: rgb(%s)! important;" % (', '.join(col))
269 269
270 270 def url_func(repo_name):
271 271
272 272 def _url_func(changeset):
273 273 author = changeset.author
274 274 date = changeset.date
275 275 message = tooltip(changeset.message)
276 276
277 277 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
278 278 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
279 279 "</b> %s<br/></div>")
280 280
281 281 tooltip_html = tooltip_html % (author, date, message)
282 282 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
283 283 short_id(changeset.raw_id))
284 284 uri = link_to(
285 285 lnk_format,
286 286 url('changeset_home', repo_name=repo_name,
287 287 revision=changeset.raw_id),
288 288 style=get_color_string(changeset.raw_id),
289 289 class_='tooltip',
290 290 title=tooltip_html
291 291 )
292 292
293 293 uri += '\n'
294 294 return uri
295 295 return _url_func
296 296
297 297 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
298 298
299 299 def get_changeset_safe(repo, rev):
300 300 from vcs.backends.base import BaseRepository
301 301 from vcs.exceptions import RepositoryError
302 302 if not isinstance(repo, BaseRepository):
303 303 raise Exception('You must pass an Repository '
304 304 'object as first argument got %s', type(repo))
305 305
306 306 try:
307 307 cs = repo.get_changeset(rev)
308 308 except RepositoryError:
309 309 from rhodecode.lib.utils import EmptyChangeset
310 310 cs = EmptyChangeset()
311 311 return cs
312 312
313 313
314 314 def is_following_repo(repo_name, user_id):
315 315 from rhodecode.model.scm import ScmModel
316 316 return ScmModel().is_following_repo(repo_name, user_id)
317 317
318 318 flash = _Flash()
319 319
320 320 #==============================================================================
321 321 # SCM FILTERS available via h.
322 322 #==============================================================================
323 323 from vcs.utils import author_name, author_email
324 324 from rhodecode.lib import credentials_filter, age as _age
325 325
326 326 age = lambda x:_age(x)
327 327 capitalize = lambda x: x.capitalize()
328 328 email = author_email
329 329 email_or_none = lambda x: email(x) if email(x) != x else None
330 330 person = lambda x: author_name(x)
331 331 short_id = lambda x: x[:12]
332 332 hide_credentials = lambda x: ''.join(credentials_filter(x))
333 333
334 334 def bool2icon(value):
335 335 """Returns True/False values represented as small html image of true/false
336 336 icons
337 337
338 338 :param value: bool value
339 339 """
340 340
341 341 if value is True:
342 342 return HTML.tag('img', src=url("/images/icons/accept.png"),
343 343 alt=_('True'))
344 344
345 345 if value is False:
346 346 return HTML.tag('img', src=url("/images/icons/cancel.png"),
347 347 alt=_('False'))
348 348
349 349 return value
350 350
351 351
352 352 def action_parser(user_log, feed=False):
353 353 """This helper will action_map the specified string action into translated
354 354 fancy names with icons and links
355 355
356 356 :param user_log: user log instance
357 357 :param feed: use output for feeds (no html and fancy icons)
358 358 """
359 359
360 360 action = user_log.action
361 361 action_params = ' '
362 362
363 363 x = action.split(':')
364 364
365 365 if len(x) > 1:
366 366 action, action_params = x
367 367
368 368 def get_cs_links():
369 369 revs_limit = 5 #display this amount always
370 370 revs_top_limit = 50 #show upto this amount of changesets hidden
371 371 revs = action_params.split(',')
372 372 repo_name = user_log.repository.repo_name
373 373
374 374 from rhodecode.model.scm import ScmModel
375 375 repo = user_log.repository.scm_instance
376 376
377 377 message = lambda rev: get_changeset_safe(repo, rev).message
378 378 cs_links = []
379 379 cs_links.append(" " + ', '.join ([link_to(rev,
380 380 url('changeset_home',
381 381 repo_name=repo_name,
382 382 revision=rev), title=tooltip(message(rev)),
383 383 class_='tooltip') for rev in revs[:revs_limit] ]))
384 384
385 385 compare_view = (' <div class="compare_view tooltip" title="%s">'
386 386 '<a href="%s">%s</a> '
387 387 '</div>' % (_('Show all combined changesets %s->%s' \
388 388 % (revs[0], revs[-1])),
389 389 url('changeset_home', repo_name=repo_name,
390 390 revision='%s...%s' % (revs[0], revs[-1])
391 391 ),
392 392 _('compare view'))
393 393 )
394 394
395 395 if len(revs) > revs_limit:
396 396 uniq_id = revs[0]
397 397 html_tmpl = ('<span> %s '
398 398 '<a class="show_more" id="_%s" href="#more">%s</a> '
399 399 '%s</span>')
400 400 if not feed:
401 401 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
402 402 % (len(revs) - revs_limit),
403 403 _('revisions')))
404 404
405 405 if not feed:
406 406 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
407 407 else:
408 408 html_tmpl = '<span id="%s"> %s </span>'
409 409
410 410 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
411 411 url('changeset_home',
412 412 repo_name=repo_name, revision=rev),
413 413 title=message(rev), class_='tooltip')
414 414 for rev in revs[revs_limit:revs_top_limit]])))
415 415 if len(revs) > 1:
416 416 cs_links.append(compare_view)
417 417 return ''.join(cs_links)
418 418
419 419 def get_fork_name():
420 420 repo_name = action_params
421 421 return _('fork name ') + str(link_to(action_params, url('summary_home',
422 422 repo_name=repo_name,)))
423 423
424 424 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
425 425 'user_created_repo':(_('[created] repository'), None),
426 426 'user_forked_repo':(_('[forked] repository'), get_fork_name),
427 427 'user_updated_repo':(_('[updated] repository'), None),
428 428 'admin_deleted_repo':(_('[delete] repository'), None),
429 429 'admin_created_repo':(_('[created] repository'), None),
430 430 'admin_forked_repo':(_('[forked] repository'), None),
431 431 'admin_updated_repo':(_('[updated] repository'), None),
432 432 'push':(_('[pushed] into'), get_cs_links),
433 433 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
434 434 'push_remote':(_('[pulled from remote] into'), get_cs_links),
435 435 'pull':(_('[pulled] from'), None),
436 436 'started_following_repo':(_('[started following] repository'), None),
437 437 'stopped_following_repo':(_('[stopped following] repository'), None),
438 438 }
439 439
440 440 action_str = action_map.get(action, action)
441 441 if feed:
442 442 action = action_str[0].replace('[', '').replace(']', '')
443 443 else:
444 444 action = action_str[0].replace('[', '<span class="journal_highlight">')\
445 445 .replace(']', '</span>')
446 446
447 447 action_params_func = lambda :""
448 448
449 449 if callable(action_str[1]):
450 450 action_params_func = action_str[1]
451 451
452 452 return [literal(action), action_params_func]
453 453
454 454 def action_parser_icon(user_log):
455 455 action = user_log.action
456 456 action_params = None
457 457 x = action.split(':')
458 458
459 459 if len(x) > 1:
460 460 action, action_params = x
461 461
462 462 tmpl = """<img src="%s%s" alt="%s"/>"""
463 463 map = {'user_deleted_repo':'database_delete.png',
464 464 'user_created_repo':'database_add.png',
465 465 'user_forked_repo':'arrow_divide.png',
466 466 'user_updated_repo':'database_edit.png',
467 467 'admin_deleted_repo':'database_delete.png',
468 468 'admin_created_repo':'database_add.png',
469 469 'admin_forked_repo':'arrow_divide.png',
470 470 'admin_updated_repo':'database_edit.png',
471 471 'push':'script_add.png',
472 472 'push_local':'script_edit.png',
473 473 'push_remote':'connect.png',
474 474 'pull':'down_16.png',
475 475 'started_following_repo':'heart_add.png',
476 476 'stopped_following_repo':'heart_delete.png',
477 477 }
478 478 return literal(tmpl % ((url('/images/icons/')),
479 479 map.get(action, action), action))
480 480
481 481
482 482 #==============================================================================
483 483 # PERMS
484 484 #==============================================================================
485 485 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
486 486 HasRepoPermissionAny, HasRepoPermissionAll
487 487
488 488 #==============================================================================
489 489 # GRAVATAR URL
490 490 #==============================================================================
491 491
492 492 def gravatar_url(email_address, size=30):
493 493 if not str2bool(config['app_conf'].get('use_gravatar')) or \
494 494 email_address == 'anonymous@rhodecode.org':
495 495 return "/images/user%s.png" % size
496 496
497 497 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
498 498 default = 'identicon'
499 499 baseurl_nossl = "http://www.gravatar.com/avatar/"
500 500 baseurl_ssl = "https://secure.gravatar.com/avatar/"
501 501 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
502 502
503 503 if isinstance(email_address, unicode):
504 504 #hashlib crashes on unicode items
505 email_address = email_address.encode('utf8', 'replace')
505 email_address = safe_str(email_address)
506 506 # construct the url
507 507 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
508 508 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
509 509
510 510 return gravatar_url
511 511
512 512
513 513 #==============================================================================
514 514 # REPO PAGER, PAGER FOR REPOSITORY
515 515 #==============================================================================
516 516 class RepoPage(Page):
517 517
518 518 def __init__(self, collection, page=1, items_per_page=20,
519 519 item_count=None, url=None, branch_name=None, **kwargs):
520 520
521 521 """Create a "RepoPage" instance. special pager for paging
522 522 repository
523 523 """
524 524 self._url_generator = url
525 525
526 526 # Safe the kwargs class-wide so they can be used in the pager() method
527 527 self.kwargs = kwargs
528 528
529 529 # Save a reference to the collection
530 530 self.original_collection = collection
531 531
532 532 self.collection = collection
533 533
534 534 # The self.page is the number of the current page.
535 535 # The first page has the number 1!
536 536 try:
537 537 self.page = int(page) # make it int() if we get it as a string
538 538 except (ValueError, TypeError):
539 539 self.page = 1
540 540
541 541 self.items_per_page = items_per_page
542 542
543 543 # Unless the user tells us how many items the collections has
544 544 # we calculate that ourselves.
545 545 if item_count is not None:
546 546 self.item_count = item_count
547 547 else:
548 548 self.item_count = len(self.collection)
549 549
550 550 # Compute the number of the first and last available page
551 551 if self.item_count > 0:
552 552 self.first_page = 1
553 553 self.page_count = ((self.item_count - 1) / self.items_per_page) + 1
554 554 self.last_page = self.first_page + self.page_count - 1
555 555
556 556 # Make sure that the requested page number is the range of valid pages
557 557 if self.page > self.last_page:
558 558 self.page = self.last_page
559 559 elif self.page < self.first_page:
560 560 self.page = self.first_page
561 561
562 562 # Note: the number of items on this page can be less than
563 563 # items_per_page if the last page is not full
564 564 self.first_item = max(0, (self.item_count) - (self.page * items_per_page))
565 565 self.last_item = ((self.item_count - 1) - items_per_page * (self.page - 1))
566 566
567 567 iterator = self.collection.get_changesets(start=self.first_item,
568 568 end=self.last_item,
569 569 reverse=True,
570 570 branch_name=branch_name)
571 571 self.items = list(iterator)
572 572
573 573 # Links to previous and next page
574 574 if self.page > self.first_page:
575 575 self.previous_page = self.page - 1
576 576 else:
577 577 self.previous_page = None
578 578
579 579 if self.page < self.last_page:
580 580 self.next_page = self.page + 1
581 581 else:
582 582 self.next_page = None
583 583
584 584 # No items available
585 585 else:
586 586 self.first_page = None
587 587 self.page_count = 0
588 588 self.last_page = None
589 589 self.first_item = None
590 590 self.last_item = None
591 591 self.previous_page = None
592 592 self.next_page = None
593 593 self.items = []
594 594
595 595 # This is a subclass of the 'list' type. Initialise the list now.
596 596 list.__init__(self, self.items)
597 597
598 598
599 599 def changed_tooltip(nodes):
600 600 """
601 601 Generates a html string for changed nodes in changeset page.
602 602 It limits the output to 30 entries
603 603
604 604 :param nodes: LazyNodesGenerator
605 605 """
606 606 if nodes:
607 607 pref = ': <br/> '
608 608 suf = ''
609 609 if len(nodes) > 30:
610 610 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
611 611 return literal(pref + '<br/> '.join([safe_unicode(x.path)
612 612 for x in nodes[:30]]) + suf)
613 613 else:
614 614 return ': ' + _('No Files')
615 615
616 616
617 617
618 618 def repo_link(groups_and_repos):
619 619 """
620 620 Makes a breadcrumbs link to repo within a group
621 621 joins &raquo; on each group to create a fancy link
622 622
623 623 ex::
624 624 group >> subgroup >> repo
625 625
626 626 :param groups_and_repos:
627 627 """
628 628 groups, repo_name = groups_and_repos
629 629
630 630 if not groups:
631 631 return repo_name
632 632 else:
633 633 def make_link(group):
634 634 return link_to(group.group_name, url('repos_group',
635 635 id=group.group_id))
636 636 return literal(' &raquo; '.join(map(make_link, groups)) + \
637 637 " &raquo; " + repo_name)
638 638
639 639
640 640 def fancy_file_stats(stats):
641 641 """
642 642 Displays a fancy two colored bar for number of added/deleted
643 643 lines of code on file
644 644
645 645 :param stats: two element list of added/deleted lines of code
646 646 """
647 647
648 648 a, d, t = stats[0], stats[1], stats[0] + stats[1]
649 649 width = 100
650 650 unit = float(width) / (t or 1)
651 651
652 652 # needs > 9% of width to be visible or 0 to be hidden
653 653 a_p = max(9, unit * a) if a > 0 else 0
654 654 d_p = max(9, unit * d) if d > 0 else 0
655 655 p_sum = a_p + d_p
656 656
657 657 if p_sum > width:
658 658 #adjust the percentage to be == 100% since we adjusted to 9
659 659 if a_p > d_p:
660 660 a_p = a_p - (p_sum - width)
661 661 else:
662 662 d_p = d_p - (p_sum - width)
663 663
664 664 a_v = a if a > 0 else ''
665 665 d_v = d if d > 0 else ''
666 666
667 667
668 668 def cgen(l_type):
669 669 mapping = {'tr':'top-right-rounded-corner',
670 670 'tl':'top-left-rounded-corner',
671 671 'br':'bottom-right-rounded-corner',
672 672 'bl':'bottom-left-rounded-corner'}
673 673 map_getter = lambda x:mapping[x]
674 674
675 675 if l_type == 'a' and d_v:
676 676 #case when added and deleted are present
677 677 return ' '.join(map(map_getter, ['tl', 'bl']))
678 678
679 679 if l_type == 'a' and not d_v:
680 680 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
681 681
682 682 if l_type == 'd' and a_v:
683 683 return ' '.join(map(map_getter, ['tr', 'br']))
684 684
685 685 if l_type == 'd' and not a_v:
686 686 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
687 687
688 688
689 689
690 690 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
691 691 a_p, a_v)
692 692 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
693 693 d_p, d_v)
694 694 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
@@ -1,275 +1,276 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from dulwich import server as dulserver
32 32
33 33
34 34 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35 35
36 36 def handle(self):
37 37 write = lambda x: self.proto.write_sideband(1, x)
38 38
39 39 graph_walker = dulserver.ProtocolGraphWalker(self,
40 40 self.repo.object_store,
41 41 self.repo.get_peeled)
42 42 objects_iter = self.repo.fetch_objects(
43 43 graph_walker.determine_wants, graph_walker, self.progress,
44 44 get_tagged=self.get_tagged)
45 45
46 46 # Do they want any objects?
47 47 if len(objects_iter) == 0:
48 48 return
49 49
50 50 self.progress("counting objects: %d, done.\n" % len(objects_iter))
51 51 dulserver.write_pack_data(dulserver.ProtocolFile(None, write),
52 52 objects_iter, len(objects_iter))
53 53 messages = []
54 54 messages.append('thank you for using rhodecode')
55 55
56 56 for msg in messages:
57 57 self.progress(msg + "\n")
58 58 # we are done
59 59 self.proto.write("0000")
60 60
61 61 dulserver.DEFAULT_HANDLERS = {
62 62 'git-upload-pack': SimpleGitUploadPackHandler,
63 63 'git-receive-pack': dulserver.ReceivePackHandler,
64 64 }
65 65
66 66 from dulwich.repo import Repo
67 67 from dulwich.web import HTTPGitApplication
68 68
69 69 from paste.auth.basic import AuthBasicAuthenticator
70 70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
71 71
72 from rhodecode.lib import safe_str
72 73 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
73 74 from rhodecode.lib.utils import invalidate_cache, check_repo_fast
74 75 from rhodecode.model.user import UserModel
75 76
76 77 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
77 78
78 79 log = logging.getLogger(__name__)
79 80
80 81
81 82 def is_git(environ):
82 83 """Returns True if request's target is git server.
83 84 ``HTTP_USER_AGENT`` would then have git client version given.
84 85
85 86 :param environ:
86 87 """
87 88 http_user_agent = environ.get('HTTP_USER_AGENT')
88 89 if http_user_agent and http_user_agent.startswith('git'):
89 90 return True
90 91 return False
91 92
92 93
93 94 class SimpleGit(object):
94 95
95 96 def __init__(self, application, config):
96 97 self.application = application
97 98 self.config = config
98 99 #authenticate this git request using
99 100 self.authenticate = AuthBasicAuthenticator('', authfunc)
100 101 self.ipaddr = '0.0.0.0'
101 102 self.repo_name = None
102 103 self.username = None
103 104 self.action = None
104 105
105 106 def __call__(self, environ, start_response):
106 107 if not is_git(environ):
107 108 return self.application(environ, start_response)
108 109
109 110 proxy_key = 'HTTP_X_REAL_IP'
110 111 def_key = 'REMOTE_ADDR'
111 112 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
112 113 # skip passing error to error controller
113 114 environ['pylons.status_code_redirect'] = True
114 115
115 116 #======================================================================
116 117 # GET ACTION PULL or PUSH
117 118 #======================================================================
118 119 self.action = self.__get_action(environ)
119 120 try:
120 121 #==================================================================
121 122 # GET REPOSITORY NAME
122 123 #==================================================================
123 124 self.repo_name = self.__get_repository(environ)
124 125 except:
125 126 return HTTPInternalServerError()(environ, start_response)
126 127
127 128 #======================================================================
128 129 # CHECK ANONYMOUS PERMISSION
129 130 #======================================================================
130 131 if self.action in ['pull', 'push']:
131 132 anonymous_user = self.__get_user('default')
132 133 self.username = anonymous_user.username
133 134 anonymous_perm = self.__check_permission(self.action,
134 135 anonymous_user,
135 136 self.repo_name)
136 137
137 138 if anonymous_perm is not True or anonymous_user.active is False:
138 139 if anonymous_perm is not True:
139 140 log.debug('Not enough credentials to access this '
140 141 'repository as anonymous user')
141 142 if anonymous_user.active is False:
142 143 log.debug('Anonymous access is disabled, running '
143 144 'authentication')
144 145 #==============================================================
145 146 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
146 147 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
147 148 #==============================================================
148 149
149 150 if not REMOTE_USER(environ):
150 self.authenticate.realm = self.config['rhodecode_realm'].\
151 encode('utf8', 'replace')
151 self.authenticate.realm = \
152 safe_str(self.config['rhodecode_realm'])
152 153 result = self.authenticate(environ)
153 154 if isinstance(result, str):
154 155 AUTH_TYPE.update(environ, 'basic')
155 156 REMOTE_USER.update(environ, result)
156 157 else:
157 158 return result.wsgi_application(environ, start_response)
158 159
159 160 #==============================================================
160 161 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
161 162 # BASIC AUTH
162 163 #==============================================================
163 164
164 165 if self.action in ['pull', 'push']:
165 166 username = REMOTE_USER(environ)
166 167 try:
167 168 user = self.__get_user(username)
168 169 self.username = user.username
169 170 except:
170 171 log.error(traceback.format_exc())
171 172 return HTTPInternalServerError()(environ,
172 173 start_response)
173 174
174 175 #check permissions for this repository
175 176 perm = self.__check_permission(self.action, user,
176 177 self.repo_name)
177 178 if perm is not True:
178 179 return HTTPForbidden()(environ, start_response)
179 180
180 181 self.extras = {'ip': self.ipaddr,
181 182 'username': self.username,
182 183 'action': self.action,
183 184 'repository': self.repo_name}
184 185
185 186 #===================================================================
186 187 # GIT REQUEST HANDLING
187 188 #===================================================================
188 189 self.basepath = self.config['base_path']
189 190 self.repo_path = os.path.join(self.basepath, self.repo_name)
190 191 #quick check if that dir exists...
191 192 if check_repo_fast(self.repo_name, self.basepath):
192 193 return HTTPNotFound()(environ, start_response)
193 194 try:
194 195 app = self.__make_app()
195 196 except:
196 197 log.error(traceback.format_exc())
197 198 return HTTPInternalServerError()(environ, start_response)
198 199
199 200 #invalidate cache on push
200 201 if self.action == 'push':
201 202 self.__invalidate_cache(self.repo_name)
202 203
203 204 return app(environ, start_response)
204 205
205 206 def __make_app(self):
206 207 _d = {'/' + self.repo_name: Repo(self.repo_path)}
207 208 backend = dulserver.DictBackend(_d)
208 209 gitserve = HTTPGitApplication(backend)
209 210
210 211 return gitserve
211 212
212 213 def __check_permission(self, action, user, repo_name):
213 214 """Checks permissions using action (push/pull) user and repository
214 215 name
215 216
216 217 :param action: push or pull action
217 218 :param user: user instance
218 219 :param repo_name: repository name
219 220 """
220 221 if action == 'push':
221 222 if not HasPermissionAnyMiddleware('repository.write',
222 223 'repository.admin')(user,
223 224 repo_name):
224 225 return False
225 226
226 227 else:
227 228 #any other action need at least read permission
228 229 if not HasPermissionAnyMiddleware('repository.read',
229 230 'repository.write',
230 231 'repository.admin')(user,
231 232 repo_name):
232 233 return False
233 234
234 235 return True
235 236
236 237 def __get_repository(self, environ):
237 238 """Get's repository name out of PATH_INFO header
238 239
239 240 :param environ: environ where PATH_INFO is stored
240 241 """
241 242 try:
242 243 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
243 244 if repo_name.endswith('/'):
244 245 repo_name = repo_name.rstrip('/')
245 246 except:
246 247 log.error(traceback.format_exc())
247 248 raise
248 249 repo_name = repo_name.split('/')[0]
249 250 return repo_name
250 251
251 252 def __get_user(self, username):
252 253 return UserModel().get_by_username(username, cache=True)
253 254
254 255 def __get_action(self, environ):
255 256 """Maps git request commands into a pull or push command.
256 257
257 258 :param environ:
258 259 """
259 260 service = environ['QUERY_STRING'].split('=')
260 261 if len(service) > 1:
261 262 service_cmd = service[1]
262 263 mapping = {'git-receive-pack': 'push',
263 264 'git-upload-pack': 'pull',
264 265 }
265 266
266 267 return mapping.get(service_cmd,
267 268 service_cmd if service_cmd else 'other')
268 269 else:
269 270 return 'other'
270 271
271 272 def __invalidate_cache(self, repo_name):
272 273 """we know that some change was made to repositories and we should
273 274 invalidate the cache to see the changes right away but only for
274 275 push requests"""
275 276 invalidate_cache('get_repo_cached_%s' % repo_name)
@@ -1,279 +1,280 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from mercurial.error import RepoError
32 32 from mercurial.hgweb import hgweb
33 33 from mercurial.hgweb.request import wsgiapplication
34 34
35 35 from paste.auth.basic import AuthBasicAuthenticator
36 36 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
37 37
38 from rhodecode.lib import safe_str
38 39 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
39 40 from rhodecode.lib.utils import make_ui, invalidate_cache, \
40 41 check_repo_fast, ui_sections
41 42 from rhodecode.model.user import UserModel
42 43
43 44 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48
48 49 def is_mercurial(environ):
49 50 """Returns True if request's target is mercurial server - header
50 51 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
51 52 """
52 53 http_accept = environ.get('HTTP_ACCEPT')
53 54 if http_accept and http_accept.startswith('application/mercurial'):
54 55 return True
55 56 return False
56 57
57 58
58 59 class SimpleHg(object):
59 60
60 61 def __init__(self, application, config):
61 62 self.application = application
62 63 self.config = config
63 64 #authenticate this mercurial request using authfunc
64 65 self.authenticate = AuthBasicAuthenticator('', authfunc)
65 66 self.ipaddr = '0.0.0.0'
66 67 self.repo_name = None
67 68 self.username = None
68 69 self.action = None
69 70
70 71 def __call__(self, environ, start_response):
71 72 if not is_mercurial(environ):
72 73 return self.application(environ, start_response)
73 74
74 75 proxy_key = 'HTTP_X_REAL_IP'
75 76 def_key = 'REMOTE_ADDR'
76 77 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
77 78 # skip passing error to error controller
78 79 environ['pylons.status_code_redirect'] = True
79 80
80 81 #======================================================================
81 82 # GET ACTION PULL or PUSH
82 83 #======================================================================
83 84 self.action = self.__get_action(environ)
84 85 try:
85 86 #==================================================================
86 87 # GET REPOSITORY NAME
87 88 #==================================================================
88 89 self.repo_name = self.__get_repository(environ)
89 90 except:
90 91 return HTTPInternalServerError()(environ, start_response)
91 92
92 93 #======================================================================
93 94 # CHECK ANONYMOUS PERMISSION
94 95 #======================================================================
95 96 if self.action in ['pull', 'push']:
96 97 anonymous_user = self.__get_user('default')
97 98 self.username = anonymous_user.username
98 99 anonymous_perm = self.__check_permission(self.action,
99 100 anonymous_user,
100 101 self.repo_name)
101 102
102 103 if anonymous_perm is not True or anonymous_user.active is False:
103 104 if anonymous_perm is not True:
104 105 log.debug('Not enough credentials to access this '
105 106 'repository as anonymous user')
106 107 if anonymous_user.active is False:
107 108 log.debug('Anonymous access is disabled, running '
108 109 'authentication')
109 110 #==============================================================
110 111 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
111 112 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
112 113 #==============================================================
113 114
114 115 if not REMOTE_USER(environ):
115 self.authenticate.realm = self.config['rhodecode_realm'].\
116 encode('utf8', 'replace')
116 self.authenticate.realm = \
117 safe_str(self.config['rhodecode_realm'])
117 118 result = self.authenticate(environ)
118 119 if isinstance(result, str):
119 120 AUTH_TYPE.update(environ, 'basic')
120 121 REMOTE_USER.update(environ, result)
121 122 else:
122 123 return result.wsgi_application(environ, start_response)
123 124
124 125 #==============================================================
125 126 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
126 127 # BASIC AUTH
127 128 #==============================================================
128 129
129 130 if self.action in ['pull', 'push']:
130 131 username = REMOTE_USER(environ)
131 132 try:
132 133 user = self.__get_user(username)
133 134 self.username = user.username
134 135 except:
135 136 log.error(traceback.format_exc())
136 137 return HTTPInternalServerError()(environ,
137 138 start_response)
138 139
139 140 #check permissions for this repository
140 141 perm = self.__check_permission(self.action, user,
141 142 self.repo_name)
142 143 if perm is not True:
143 144 return HTTPForbidden()(environ, start_response)
144 145
145 146 self.extras = {'ip': self.ipaddr,
146 147 'username': self.username,
147 148 'action': self.action,
148 149 'repository': self.repo_name}
149 150
150 151 #======================================================================
151 152 # MERCURIAL REQUEST HANDLING
152 153 #======================================================================
153 154 environ['PATH_INFO'] = '/' # since we wrap into hgweb, reset the path
154 155 self.baseui = make_ui('db')
155 156 self.basepath = self.config['base_path']
156 157 self.repo_path = os.path.join(self.basepath, self.repo_name)
157 158
158 159 #quick check if that dir exists...
159 160 if check_repo_fast(self.repo_name, self.basepath):
160 161 return HTTPNotFound()(environ, start_response)
161 162 try:
162 163 app = wsgiapplication(self.__make_app)
163 164 except RepoError, e:
164 165 if str(e).find('not found') != -1:
165 166 return HTTPNotFound()(environ, start_response)
166 167 except Exception:
167 168 log.error(traceback.format_exc())
168 169 return HTTPInternalServerError()(environ, start_response)
169 170
170 171 #invalidate cache on push
171 172 if self.action == 'push':
172 173 self.__invalidate_cache(self.repo_name)
173 174
174 175 return app(environ, start_response)
175 176
176 177 def __make_app(self):
177 178 """
178 179 Make an wsgi application using hgweb, and inject generated baseui
179 180 instance, additionally inject some extras into ui object
180 181 """
181 182 self.__inject_extras(self.baseui, self.extras)
182 183 return hgweb(str(self.repo_path), baseui=self.baseui)
183 184
184 185
185 186 def __check_permission(self, action, user, repo_name):
186 187 """
187 188 Checks permissions using action (push/pull) user and repository
188 189 name
189 190
190 191 :param action: push or pull action
191 192 :param user: user instance
192 193 :param repo_name: repository name
193 194 """
194 195 if action == 'push':
195 196 if not HasPermissionAnyMiddleware('repository.write',
196 197 'repository.admin')(user,
197 198 repo_name):
198 199 return False
199 200
200 201 else:
201 202 #any other action need at least read permission
202 203 if not HasPermissionAnyMiddleware('repository.read',
203 204 'repository.write',
204 205 'repository.admin')(user,
205 206 repo_name):
206 207 return False
207 208
208 209 return True
209 210
210 211 def __get_repository(self, environ):
211 212 """
212 213 Get's repository name out of PATH_INFO header
213 214
214 215 :param environ: environ where PATH_INFO is stored
215 216 """
216 217 try:
217 218 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
218 219 if repo_name.endswith('/'):
219 220 repo_name = repo_name.rstrip('/')
220 221 except:
221 222 log.error(traceback.format_exc())
222 223 raise
223 224
224 225 return repo_name
225 226
226 227 def __get_user(self, username):
227 228 return UserModel().get_by_username(username, cache=True)
228 229
229 230 def __get_action(self, environ):
230 231 """
231 232 Maps mercurial request commands into a clone,pull or push command.
232 233 This should always return a valid command string
233 234
234 235 :param environ:
235 236 """
236 237 mapping = {'changegroup': 'pull',
237 238 'changegroupsubset': 'pull',
238 239 'stream_out': 'pull',
239 240 'listkeys': 'pull',
240 241 'unbundle': 'push',
241 242 'pushkey': 'push', }
242 243 for qry in environ['QUERY_STRING'].split('&'):
243 244 if qry.startswith('cmd'):
244 245 cmd = qry.split('=')[-1]
245 246 if cmd in mapping:
246 247 return mapping[cmd]
247 248 else:
248 249 return 'pull'
249 250
250 251 def __invalidate_cache(self, repo_name):
251 252 """we know that some change was made to repositories and we should
252 253 invalidate the cache to see the changes right away but only for
253 254 push requests"""
254 255 invalidate_cache('get_repo_cached_%s' % repo_name)
255 256
256 257 def __inject_extras(self, baseui, extras={}):
257 258 """
258 259 Injects some extra params into baseui instance
259 260
260 261 also overwrites global settings with those takes from local hgrc file
261 262
262 263 :param baseui: baseui instance
263 264 :param extras: dict with extra params to put into baseui
264 265 """
265 266
266 267 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
267 268
268 269 #inject some additional parameters that will be available in ui
269 270 #for hooks
270 271 for k, v in extras.items():
271 272 baseui.setconfig('rhodecode_extras', k, v)
272 273
273 274 repoui = make_ui('file', hgrc, False)
274 275
275 276 if repoui:
276 277 #overwrite our ui instance with the section from hgrc file
277 278 for section in ui_sections:
278 279 for k, v in repoui.configitems(section):
279 280 baseui.setconfig(section, k, v)
@@ -1,624 +1,628 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) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import paste
31 31 import beaker
32 32 from os.path import dirname as dn, join as jn
33 33
34 34 from paste.script.command import Command, BadCommand
35 35
36 36 from UserDict import DictMixin
37 37
38 38 from mercurial import ui, config, hg
39 39 from mercurial.error import RepoError
40 40
41 41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 42
43 43 from vcs.backends.base import BaseChangeset
44 44 from vcs.utils.lazy import LazyProperty
45 45
46 46 from rhodecode.model import meta
47 47 from rhodecode.model.caching_query import FromCache
48 48 from rhodecode.model.db import Repository, User, RhodeCodeUi, UserLog, Group, \
49 49 RhodeCodeSettings
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.user import UserModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def recursive_replace(str, replace=' '):
57 57 """Recursive replace of given sign to just one instance
58 58
59 59 :param str: given string
60 60 :param replace: char to find and replace multiple instances
61 61
62 62 Examples::
63 63 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
64 64 'Mighty-Mighty-Bo-sstones'
65 65 """
66 66
67 67 if str.find(replace * 2) == -1:
68 68 return str
69 69 else:
70 70 str = str.replace(replace * 2, replace)
71 71 return recursive_replace(str, replace)
72 72
73 73
74 74 def repo_name_slug(value):
75 75 """Return slug of name of repository
76 76 This function is called on each creation/modification
77 77 of repository to prevent bad names in repo
78 78 """
79 79
80 80 slug = remove_formatting(value)
81 81 slug = strip_tags(slug)
82 82
83 83 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
84 84 slug = slug.replace(c, '-')
85 85 slug = recursive_replace(slug, '-')
86 86 slug = collapse(slug, '-')
87 87 return slug
88 88
89 89
90 90 def get_repo_slug(request):
91 91 return request.environ['pylons.routes_dict'].get('repo_name')
92 92
93 93
94 94 def action_logger(user, action, repo, ipaddr='', sa=None):
95 95 """
96 96 Action logger for various actions made by users
97 97
98 98 :param user: user that made this action, can be a unique username string or
99 99 object containing user_id attribute
100 100 :param action: action to log, should be on of predefined unique actions for
101 101 easy translations
102 102 :param repo: string name of repository or object containing repo_id,
103 103 that action was made on
104 104 :param ipaddr: optional ip address from what the action was made
105 105 :param sa: optional sqlalchemy session
106 106
107 107 """
108 108
109 109 if not sa:
110 110 sa = meta.Session()
111 111
112 112 try:
113 113 um = UserModel()
114 114 if hasattr(user, 'user_id'):
115 115 user_obj = user
116 116 elif isinstance(user, basestring):
117 117 user_obj = um.get_by_username(user, cache=False)
118 118 else:
119 119 raise Exception('You have to provide user object or username')
120 120
121 121 rm = RepoModel()
122 122 if hasattr(repo, 'repo_id'):
123 123 repo_obj = rm.get(repo.repo_id, cache=False)
124 124 repo_name = repo_obj.repo_name
125 125 elif isinstance(repo, basestring):
126 126 repo_name = repo.lstrip('/')
127 127 repo_obj = rm.get_by_repo_name(repo_name, cache=False)
128 128 else:
129 129 raise Exception('You have to provide repository to action logger')
130 130
131 131 user_log = UserLog()
132 132 user_log.user_id = user_obj.user_id
133 133 user_log.action = action
134 134
135 135 user_log.repository_id = repo_obj.repo_id
136 136 user_log.repository_name = repo_name
137 137
138 138 user_log.action_date = datetime.datetime.now()
139 139 user_log.user_ip = ipaddr
140 140 sa.add(user_log)
141 141 sa.commit()
142 142
143 143 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
144 144 except:
145 145 log.error(traceback.format_exc())
146 146 sa.rollback()
147 147
148 148
149 149 def get_repos(path, recursive=False):
150 150 """
151 151 Scans given path for repos and return (name,(type,path)) tuple
152 152
153 153 :param path: path to scann for repositories
154 154 :param recursive: recursive search and return names with subdirs in front
155 155 """
156 156 from vcs.utils.helpers import get_scm
157 157 from vcs.exceptions import VCSError
158 158
159 159 if path.endswith(os.sep):
160 160 #remove ending slash for better results
161 161 path = path[:-1]
162 162
163 163 def _get_repos(p):
164 164 if not os.access(p, os.W_OK):
165 165 return
166 166 for dirpath in os.listdir(p):
167 167 if os.path.isfile(os.path.join(p, dirpath)):
168 168 continue
169 169 cur_path = os.path.join(p, dirpath)
170 170 try:
171 171 scm_info = get_scm(cur_path)
172 172 yield scm_info[1].split(path)[-1].lstrip(os.sep), scm_info
173 173 except VCSError:
174 174 if not recursive:
175 175 continue
176 176 #check if this dir containts other repos for recursive scan
177 177 rec_path = os.path.join(p, dirpath)
178 178 if os.path.isdir(rec_path):
179 179 for inner_scm in _get_repos(rec_path):
180 180 yield inner_scm
181 181
182 182 return _get_repos(path)
183 183
184 184
185 185 def check_repo_fast(repo_name, base_path):
186 186 """
187 187 Check given path for existence of directory
188 188 :param repo_name:
189 189 :param base_path:
190 190
191 191 :return False: if this directory is present
192 192 """
193 193 if os.path.isdir(os.path.join(base_path, repo_name)):
194 194 return False
195 195 return True
196 196
197 197
198 198 def check_repo(repo_name, base_path, verify=True):
199 199
200 200 repo_path = os.path.join(base_path, repo_name)
201 201
202 202 try:
203 203 if not check_repo_fast(repo_name, base_path):
204 204 return False
205 205 r = hg.repository(ui.ui(), repo_path)
206 206 if verify:
207 207 hg.verify(r)
208 208 #here we hnow that repo exists it was verified
209 209 log.info('%s repo is already created', repo_name)
210 210 return False
211 211 except RepoError:
212 212 #it means that there is no valid repo there...
213 213 log.info('%s repo is free for creation', repo_name)
214 214 return True
215 215
216 216
217 217 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
218 218 while True:
219 219 ok = raw_input(prompt)
220 220 if ok in ('y', 'ye', 'yes'):
221 221 return True
222 222 if ok in ('n', 'no', 'nop', 'nope'):
223 223 return False
224 224 retries = retries - 1
225 225 if retries < 0:
226 226 raise IOError
227 227 print complaint
228 228
229 229 #propagated from mercurial documentation
230 230 ui_sections = ['alias', 'auth',
231 231 'decode/encode', 'defaults',
232 232 'diff', 'email',
233 233 'extensions', 'format',
234 234 'merge-patterns', 'merge-tools',
235 235 'hooks', 'http_proxy',
236 236 'smtp', 'patch',
237 237 'paths', 'profiling',
238 238 'server', 'trusted',
239 239 'ui', 'web', ]
240 240
241 241
242 242 def make_ui(read_from='file', path=None, checkpaths=True):
243 243 """A function that will read python rc files or database
244 244 and make an mercurial ui object from read options
245 245
246 246 :param path: path to mercurial config file
247 247 :param checkpaths: check the path
248 248 :param read_from: read from 'file' or 'db'
249 249 """
250 250
251 251 baseui = ui.ui()
252 252
253 253 #clean the baseui object
254 254 baseui._ocfg = config.config()
255 255 baseui._ucfg = config.config()
256 256 baseui._tcfg = config.config()
257 257
258 258 if read_from == 'file':
259 259 if not os.path.isfile(path):
260 260 log.warning('Unable to read config file %s' % path)
261 261 return False
262 262 log.debug('reading hgrc from %s', path)
263 263 cfg = config.config()
264 264 cfg.read(path)
265 265 for section in ui_sections:
266 266 for k, v in cfg.items(section):
267 267 log.debug('settings ui from file[%s]%s:%s', section, k, v)
268 268 baseui.setconfig(section, k, v)
269 269
270 270 elif read_from == 'db':
271 271 sa = meta.Session()
272 272 ret = sa.query(RhodeCodeUi)\
273 273 .options(FromCache("sql_cache_short",
274 274 "get_hg_ui_settings")).all()
275 275
276 276 hg_ui = ret
277 277 for ui_ in hg_ui:
278 278 if ui_.ui_active:
279 279 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
280 280 ui_.ui_key, ui_.ui_value)
281 281 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
282 282
283 283 meta.Session.remove()
284 284 return baseui
285 285
286 286
287 287 def set_rhodecode_config(config):
288 288 """Updates pylons config with new settings from database
289 289
290 290 :param config:
291 291 """
292 292 hgsettings = RhodeCodeSettings.get_app_settings()
293 293
294 294 for k, v in hgsettings.items():
295 295 config[k] = v
296 296
297 297
298 298 def invalidate_cache(cache_key, *args):
299 299 """Puts cache invalidation task into db for
300 300 further global cache invalidation
301 301 """
302 302
303 303 from rhodecode.model.scm import ScmModel
304 304
305 305 if cache_key.startswith('get_repo_cached_'):
306 306 name = cache_key.split('get_repo_cached_')[-1]
307 307 ScmModel().mark_for_invalidation(name)
308 308
309 309
310 310 class EmptyChangeset(BaseChangeset):
311 311 """
312 312 An dummy empty changeset. It's possible to pass hash when creating
313 313 an EmptyChangeset
314 314 """
315 315
316 316 def __init__(self, cs='0' * 40, repo=None):
317 317 self._empty_cs = cs
318 318 self.revision = -1
319 319 self.message = ''
320 320 self.author = ''
321 321 self.date = ''
322 322 self.repository = repo
323 323
324 324 @LazyProperty
325 325 def raw_id(self):
326 326 """Returns raw string identifying this changeset, useful for web
327 327 representation.
328 328 """
329 329
330 330 return self._empty_cs
331 331
332 332 @LazyProperty
333 333 def short_id(self):
334 334 return self.raw_id[:12]
335 335
336 336 def get_file_changeset(self, path):
337 337 return self
338 338
339 339 def get_file_content(self, path):
340 340 return u''
341 341
342 342 def get_file_size(self, path):
343 343 return 0
344 344
345 345
346 346 def map_groups(groups):
347 347 """Checks for groups existence, and creates groups structures.
348 348 It returns last group in structure
349 349
350 350 :param groups: list of groups structure
351 351 """
352 352 sa = meta.Session()
353 353
354 354 parent = None
355 355 group = None
356 356 for lvl, group_name in enumerate(groups[:-1]):
357 357 group = sa.query(Group).filter(Group.group_name == group_name).scalar()
358 358
359 359 if group is None:
360 360 group = Group(group_name, parent)
361 361 sa.add(group)
362 362 sa.commit()
363 363
364 364 parent = group
365 365
366 366 return group
367 367
368 368
369 369 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
370 370 """maps all repos given in initial_repo_list, non existing repositories
371 371 are created, if remove_obsolete is True it also check for db entries
372 372 that are not in initial_repo_list and removes them.
373 373
374 374 :param initial_repo_list: list of repositories found by scanning methods
375 375 :param remove_obsolete: check for obsolete entries in database
376 376 """
377 377
378 378 sa = meta.Session()
379 379 rm = RepoModel()
380 380 user = sa.query(User).filter(User.admin == True).first()
381 381 added = []
382 382 for name, repo in initial_repo_list.items():
383 383 group = map_groups(name.split('/'))
384 384 if not rm.get_by_repo_name(name, cache=False):
385 385 log.info('repository %s not found creating default', name)
386 386 added.append(name)
387 387 form_data = {
388 388 'repo_name': name,
389 389 'repo_name_full': name,
390 390 'repo_type': repo.alias,
391 391 'description': repo.description \
392 392 if repo.description != 'unknown' else \
393 393 '%s repository' % name,
394 394 'private': False,
395 395 'group_id': getattr(group, 'group_id', None)
396 396 }
397 397 rm.create(form_data, user, just_db=True)
398 398
399 399 removed = []
400 400 if remove_obsolete:
401 401 #remove from database those repositories that are not in the filesystem
402 402 for repo in sa.query(Repository).all():
403 403 if repo.repo_name not in initial_repo_list.keys():
404 404 removed.append(repo.repo_name)
405 405 sa.delete(repo)
406 406 sa.commit()
407 407
408 408 return added, removed
409 409
410 410 #set cache regions for beaker so celery can utilise it
411 411 def add_cache(settings):
412 412 cache_settings = {'regions': None}
413 413 for key in settings.keys():
414 414 for prefix in ['beaker.cache.', 'cache.']:
415 415 if key.startswith(prefix):
416 416 name = key.split(prefix)[1].strip()
417 417 cache_settings[name] = settings[key].strip()
418 418 if cache_settings['regions']:
419 419 for region in cache_settings['regions'].split(','):
420 420 region = region.strip()
421 421 region_settings = {}
422 422 for key, value in cache_settings.items():
423 423 if key.startswith(region):
424 424 region_settings[key.split('.')[1]] = value
425 425 region_settings['expire'] = int(region_settings.get('expire',
426 426 60))
427 427 region_settings.setdefault('lock_dir',
428 428 cache_settings.get('lock_dir'))
429 429 region_settings.setdefault('data_dir',
430 430 cache_settings.get('data_dir'))
431 431
432 432 if 'type' not in region_settings:
433 433 region_settings['type'] = cache_settings.get('type',
434 434 'memory')
435 435 beaker.cache.cache_regions[region] = region_settings
436 436
437 437
438 438 def get_current_revision():
439 439 """Returns tuple of (number, id) from repository containing this package
440 440 or None if repository could not be found.
441 441 """
442 442
443 443 try:
444 444 from vcs import get_repo
445 445 from vcs.utils.helpers import get_scm
446 446 from vcs.exceptions import RepositoryError, VCSError
447 447 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
448 448 scm = get_scm(repopath)[0]
449 449 repo = get_repo(path=repopath, alias=scm)
450 450 tip = repo.get_changeset()
451 451 return (tip.revision, tip.short_id)
452 452 except (ImportError, RepositoryError, VCSError), err:
453 453 logging.debug("Cannot retrieve rhodecode's revision. Original error "
454 454 "was: %s" % err)
455 455 return None
456 456
457 457
458 458 #==============================================================================
459 459 # TEST FUNCTIONS AND CREATORS
460 460 #==============================================================================
461 def create_test_index(repo_location, full_index):
462 """Makes default test index
463 :param repo_location:
461 def create_test_index(repo_location, config, full_index):
462 """
463 Makes default test index
464
465 :param config: test config
464 466 :param full_index:
465 467 """
468
466 469 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
467 470 from rhodecode.lib.pidlock import DaemonLock, LockHeld
468 import shutil
471
472 repo_location = repo_location
469 473
470 index_location = os.path.join(repo_location, 'index')
471 if os.path.exists(index_location):
472 shutil.rmtree(index_location)
474 index_location = os.path.join(config['app_conf']['index_dir'], 'index')
475 if not os.path.exists(index_location):
476 os.makedirs(index_location)
473 477
474 478 try:
475 479 l = DaemonLock(file=jn(dn(index_location), 'make_index.lock'))
476 480 WhooshIndexingDaemon(index_location=index_location,
477 481 repo_location=repo_location)\
478 482 .run(full_index=full_index)
479 483 l.release()
480 484 except LockHeld:
481 485 pass
482 486
483 487
484 488 def create_test_env(repos_test_path, config):
485 489 """Makes a fresh database and
486 490 install test repository into tmp dir
487 491 """
488 492 from rhodecode.lib.db_manage import DbManage
489 493 from rhodecode.tests import HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, \
490 494 HG_FORK, GIT_FORK, TESTS_TMP_PATH
491 495 import tarfile
492 496 import shutil
493 497 from os.path import dirname as dn, join as jn, abspath
494 498
495 499 log = logging.getLogger('TestEnvCreator')
496 500 # create logger
497 501 log.setLevel(logging.DEBUG)
498 502 log.propagate = True
499 503 # create console handler and set level to debug
500 504 ch = logging.StreamHandler()
501 505 ch.setLevel(logging.DEBUG)
502 506
503 507 # create formatter
504 508 formatter = logging.Formatter("%(asctime)s - %(name)s -"
505 509 " %(levelname)s - %(message)s")
506 510
507 511 # add formatter to ch
508 512 ch.setFormatter(formatter)
509 513
510 514 # add ch to logger
511 515 log.addHandler(ch)
512 516
513 517 #PART ONE create db
514 518 dbconf = config['sqlalchemy.db1.url']
515 519 log.debug('making test db %s', dbconf)
516 520
517 521 # create test dir if it doesn't exist
518 522 if not os.path.isdir(repos_test_path):
519 523 log.debug('Creating testdir %s' % repos_test_path)
520 524 os.makedirs(repos_test_path)
521 525
522 526 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
523 527 tests=True)
524 528 dbmanage.create_tables(override=True)
525 529 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
526 530 dbmanage.create_default_user()
527 531 dbmanage.admin_prompt()
528 532 dbmanage.create_permissions()
529 533 dbmanage.populate_default_permissions()
530 534
531 535 #PART TWO make test repo
532 536 log.debug('making test vcs repositories')
533 537
534 538 #remove old one from previos tests
535 539 for r in [HG_REPO, GIT_REPO, NEW_HG_REPO, NEW_GIT_REPO, HG_FORK, GIT_FORK]:
536 540
537 541 if os.path.isdir(jn(TESTS_TMP_PATH, r)):
538 542 log.debug('removing %s', r)
539 543 shutil.rmtree(jn(TESTS_TMP_PATH, r))
540 544
541 545 idx_path = config['app_conf']['index_dir']
542 546 data_path = config['app_conf']['cache_dir']
543 547
544 548 #clean index and data
545 549 if idx_path and os.path.exists(idx_path):
546 550 log.debug('remove %s' % idx_path)
547 551 shutil.rmtree(idx_path)
548 552
549 553 if data_path and os.path.exists(data_path):
550 554 log.debug('remove %s' % data_path)
551 555 shutil.rmtree(data_path)
552 556
553 557 #CREATE DEFAULT HG REPOSITORY
554 558 cur_dir = dn(dn(abspath(__file__)))
555 559 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
556 560 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
557 561 tar.close()
558 562
559 563
560 564 #==============================================================================
561 565 # PASTER COMMANDS
562 566 #==============================================================================
563 567 class BasePasterCommand(Command):
564 568 """
565 569 Abstract Base Class for paster commands.
566 570
567 571 The celery commands are somewhat aggressive about loading
568 572 celery.conf, and since our module sets the `CELERY_LOADER`
569 573 environment variable to our loader, we have to bootstrap a bit and
570 574 make sure we've had a chance to load the pylons config off of the
571 575 command line, otherwise everything fails.
572 576 """
573 577 min_args = 1
574 578 min_args_error = "Please provide a paster config file as an argument."
575 579 takes_config_file = 1
576 580 requires_config_file = True
577 581
578 582 def notify_msg(self, msg, log=False):
579 583 """Make a notification to user, additionally if logger is passed
580 584 it logs this action using given logger
581 585
582 586 :param msg: message that will be printed to user
583 587 :param log: logging instance, to use to additionally log this message
584 588
585 589 """
586 590 if log and isinstance(log, logging):
587 591 log(msg)
588 592
589 593 def run(self, args):
590 594 """
591 595 Overrides Command.run
592 596
593 597 Checks for a config file argument and loads it.
594 598 """
595 599 if len(args) < self.min_args:
596 600 raise BadCommand(
597 601 self.min_args_error % {'min_args': self.min_args,
598 602 'actual_args': len(args)})
599 603
600 604 # Decrement because we're going to lob off the first argument.
601 605 # @@ This is hacky
602 606 self.min_args -= 1
603 607 self.bootstrap_config(args[0])
604 608 self.update_parser()
605 609 return super(BasePasterCommand, self).run(args[1:])
606 610
607 611 def update_parser(self):
608 612 """
609 613 Abstract method. Allows for the class's parser to be updated
610 614 before the superclass's `run` method is called. Necessary to
611 615 allow options/arguments to be passed through to the underlying
612 616 celery command.
613 617 """
614 618 raise NotImplementedError("Abstract Method.")
615 619
616 620 def bootstrap_config(self, conf):
617 621 """
618 622 Loads the pylons configuration.
619 623 """
620 624 from pylons import config as pylonsconfig
621 625
622 626 path_to_ini_file = os.path.realpath(conf)
623 627 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
624 628 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,790 +1,795 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 from datetime import date
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.exc import DatabaseError
34 34 from sqlalchemy.orm import relationship, backref, joinedload, class_mapper
35 35 from sqlalchemy.orm.interfaces import MapperExtension
36 36
37 37 from beaker.cache import cache_region, region_invalidate
38 38
39 39 from vcs import get_backend
40 40 from vcs.utils.helpers import get_scm
41 41 from vcs.exceptions import RepositoryError, VCSError
42 42 from vcs.utils.lazy import LazyProperty
43 43 from vcs.nodes import FileNode
44 44
45 from rhodecode.lib import str2bool, json
45 from rhodecode.lib import str2bool, json, safe_str
46 46 from rhodecode.model.meta import Base, Session
47 47 from rhodecode.model.caching_query import FromCache
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 #==============================================================================
52 52 # BASE CLASSES
53 53 #==============================================================================
54 54
55 55 class ModelSerializer(json.JSONEncoder):
56 56 """
57 57 Simple Serializer for JSON,
58 58
59 59 usage::
60 60
61 61 to make object customized for serialization implement a __json__
62 62 method that will return a dict for serialization into json
63 63
64 64 example::
65 65
66 66 class Task(object):
67 67
68 68 def __init__(self, name, value):
69 69 self.name = name
70 70 self.value = value
71 71
72 72 def __json__(self):
73 73 return dict(name=self.name,
74 74 value=self.value)
75 75
76 76 """
77 77
78 78 def default(self, obj):
79 79
80 80 if hasattr(obj, '__json__'):
81 81 return obj.__json__()
82 82 else:
83 83 return json.JSONEncoder.default(self, obj)
84 84
85 85 class BaseModel(object):
86 86 """Base Model for all classess
87 87
88 88 """
89 89
90 90 @classmethod
91 91 def _get_keys(cls):
92 92 """return column names for this model """
93 93 return class_mapper(cls).c.keys()
94 94
95 95 def get_dict(self):
96 96 """return dict with keys and values corresponding
97 97 to this model data """
98 98
99 99 d = {}
100 100 for k in self._get_keys():
101 101 d[k] = getattr(self, k)
102 102 return d
103 103
104 104 def get_appstruct(self):
105 105 """return list with keys and values tupples corresponding
106 106 to this model data """
107 107
108 108 l = []
109 109 for k in self._get_keys():
110 110 l.append((k, getattr(self, k),))
111 111 return l
112 112
113 113 def populate_obj(self, populate_dict):
114 114 """populate model with data from given populate_dict"""
115 115
116 116 for k in self._get_keys():
117 117 if k in populate_dict:
118 118 setattr(self, k, populate_dict[k])
119 119
120 120 @classmethod
121 121 def query(cls):
122 122 return Session.query(cls)
123 123
124 124 @classmethod
125 125 def get(cls, id_):
126 126 return Session.query(cls).get(id_)
127 127
128 128
129 129 class RhodeCodeSettings(Base, BaseModel):
130 130 __tablename__ = 'rhodecode_settings'
131 131 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
132 132 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
133 133 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
134 134 app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
135 135
136 136 def __init__(self, k='', v=''):
137 137 self.app_settings_name = k
138 138 self.app_settings_value = v
139 139
140 140 def __repr__(self):
141 141 return "<%s('%s:%s')>" % (self.__class__.__name__,
142 142 self.app_settings_name, self.app_settings_value)
143 143
144 144
145 145 @classmethod
146 146 def get_by_name(cls, ldap_key):
147 147 return Session.query(cls)\
148 148 .filter(cls.app_settings_name == ldap_key).scalar()
149 149
150 150 @classmethod
151 151 def get_app_settings(cls, cache=False):
152 152
153 153 ret = Session.query(cls)
154 154
155 155 if cache:
156 156 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
157 157
158 158 if not ret:
159 159 raise Exception('Could not get application settings !')
160 160 settings = {}
161 161 for each in ret:
162 162 settings['rhodecode_' + each.app_settings_name] = \
163 163 each.app_settings_value
164 164
165 165 return settings
166 166
167 167 @classmethod
168 168 def get_ldap_settings(cls, cache=False):
169 169 ret = Session.query(cls)\
170 170 .filter(cls.app_settings_name.startswith('ldap_'))\
171 171 .all()
172 172 fd = {}
173 173 for row in ret:
174 174 fd.update({row.app_settings_name:row.app_settings_value})
175 175
176 176 fd.update({'ldap_active':str2bool(fd.get('ldap_active'))})
177 177
178 178 return fd
179 179
180 180
181 181 class RhodeCodeUi(Base, BaseModel):
182 182 __tablename__ = 'rhodecode_ui'
183 183 __table_args__ = {'extend_existing':True}
184 184 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
185 185 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
186 186 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
187 187 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
188 188 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
189 189
190 190
191 191 @classmethod
192 192 def get_by_key(cls, key):
193 193 return Session.query(cls).filter(cls.ui_key == key)
194 194
195 195
196 196 class User(Base, BaseModel):
197 197 __tablename__ = 'users'
198 198 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
199 199 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
200 200 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
201 201 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
202 202 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
203 203 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
204 204 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
205 205 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
206 206 email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
207 207 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
208 208 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
209 209 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
210 210
211 211 user_log = relationship('UserLog', cascade='all')
212 212 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
213 213
214 214 repositories = relationship('Repository')
215 215 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
216 216 repo_to_perm = relationship('RepoToPerm', primaryjoin='RepoToPerm.user_id==User.user_id', cascade='all')
217 217
218 218 group_member = relationship('UsersGroupMember', cascade='all')
219 219
220 220 @property
221 221 def full_contact(self):
222 222 return '%s %s <%s>' % (self.name, self.lastname, self.email)
223 223
224 224 @property
225 225 def short_contact(self):
226 226 return '%s %s' % (self.name, self.lastname)
227 227
228 228 @property
229 229 def is_admin(self):
230 230 return self.admin
231 231
232 232 def __repr__(self):
233 233 try:
234 234 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
235 235 self.user_id, self.username)
236 236 except:
237 237 return self.__class__.__name__
238 238
239 239 @classmethod
240 240 def by_username(cls, username, case_insensitive=False):
241 241 if case_insensitive:
242 242 return Session.query(cls).filter(cls.username.like(username)).one()
243 243 else:
244 244 return Session.query(cls).filter(cls.username == username).one()
245 245
246 246 def update_lastlogin(self):
247 247 """Update user lastlogin"""
248 248
249 249 self.last_login = datetime.datetime.now()
250 250 Session.add(self)
251 251 Session.commit()
252 252 log.debug('updated user %s lastlogin', self.username)
253 253
254 254
255 255 class UserLog(Base, BaseModel):
256 256 __tablename__ = 'user_logs'
257 257 __table_args__ = {'extend_existing':True}
258 258 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
259 259 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
260 260 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
261 261 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
262 262 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
263 263 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
265 265
266 266 @property
267 267 def action_as_day(self):
268 268 return date(*self.action_date.timetuple()[:3])
269 269
270 270 user = relationship('User')
271 271 repository = relationship('Repository')
272 272
273 273
274 274 class UsersGroup(Base, BaseModel):
275 275 __tablename__ = 'users_groups'
276 276 __table_args__ = {'extend_existing':True}
277 277
278 278 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
279 279 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
280 280 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
281 281
282 282 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
283 283
284 284
285 285 @classmethod
286 286 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
287 287 if case_insensitive:
288 288 gr = Session.query(cls)\
289 289 .filter(cls.users_group_name.ilike(group_name))
290 290 else:
291 291 gr = Session.query(UsersGroup)\
292 292 .filter(UsersGroup.users_group_name == group_name)
293 293 if cache:
294 294 gr = gr.options(FromCache("sql_cache_short",
295 295 "get_user_%s" % group_name))
296 296 return gr.scalar()
297 297
298 298 class UsersGroupMember(Base, BaseModel):
299 299 __tablename__ = 'users_groups_members'
300 300 __table_args__ = {'extend_existing':True}
301 301
302 302 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
303 303 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
304 304 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
305 305
306 306 user = relationship('User', lazy='joined')
307 307 users_group = relationship('UsersGroup')
308 308
309 309 def __init__(self, gr_id='', u_id=''):
310 310 self.users_group_id = gr_id
311 311 self.user_id = u_id
312 312
313 313 class Repository(Base, BaseModel):
314 314 __tablename__ = 'repositories'
315 315 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
316 316
317 317 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
318 318 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
319 319 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
320 320 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
321 321 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
322 322 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
323 323 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
324 324 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
325 325 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
326 326 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
327 327
328 328 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
329 329 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
330 330
331 331
332 332 user = relationship('User')
333 333 fork = relationship('Repository', remote_side=repo_id)
334 334 group = relationship('Group')
335 335 repo_to_perm = relationship('RepoToPerm', cascade='all', order_by='RepoToPerm.repo_to_perm_id')
336 336 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
337 337 stats = relationship('Statistics', cascade='all', uselist=False)
338 338
339 339 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
340 340
341 341 logs = relationship('UserLog', cascade='all')
342 342
343 343 def __repr__(self):
344 344 return "<%s('%s:%s')>" % (self.__class__.__name__,
345 345 self.repo_id, self.repo_name)
346 346
347 347 @classmethod
348 348 def by_repo_name(cls, repo_name):
349 349 q = Session.query(cls).filter(cls.repo_name == repo_name)
350 350
351 351 q = q.options(joinedload(Repository.fork))\
352 352 .options(joinedload(Repository.user))\
353 353 .options(joinedload(Repository.group))\
354 354
355 355 return q.one()
356 356
357 357 @classmethod
358 358 def get_repo_forks(cls, repo_id):
359 359 return Session.query(cls).filter(Repository.fork_id == repo_id)
360 360
361 361 @property
362 362 def just_name(self):
363 363 return self.repo_name.split(os.sep)[-1]
364 364
365 365 @property
366 366 def groups_with_parents(self):
367 367 groups = []
368 368 if self.group is None:
369 369 return groups
370 370
371 371 cur_gr = self.group
372 372 groups.insert(0, cur_gr)
373 373 while 1:
374 374 gr = getattr(cur_gr, 'parent_group', None)
375 375 cur_gr = cur_gr.parent_group
376 376 if gr is None:
377 377 break
378 378 groups.insert(0, gr)
379 379
380 380 return groups
381 381
382 382 @property
383 383 def groups_and_repo(self):
384 384 return self.groups_with_parents, self.just_name
385 385
386 386 @LazyProperty
387 387 def repo_path(self):
388 388 """
389 389 Returns base full path for that repository means where it actually
390 390 exists on a filesystem
391 391 """
392 392 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/')
393 393 q.options(FromCache("sql_cache_short", "repository_repo_path"))
394 394 return q.one().ui_value
395 395
396 396 @property
397 397 def repo_full_path(self):
398 398 p = [self.repo_path]
399 399 # we need to split the name by / since this is how we store the
400 400 # names in the database, but that eventually needs to be converted
401 401 # into a valid system path
402 402 p += self.repo_name.split('/')
403 403 return os.path.join(*p)
404 404
405 405 @property
406 406 def _ui(self):
407 407 """
408 408 Creates an db based ui object for this repository
409 409 """
410 410 from mercurial import ui
411 411 from mercurial import config
412 412 baseui = ui.ui()
413 413
414 414 #clean the baseui object
415 415 baseui._ocfg = config.config()
416 416 baseui._ucfg = config.config()
417 417 baseui._tcfg = config.config()
418 418
419 419
420 420 ret = Session.query(RhodeCodeUi)\
421 421 .options(FromCache("sql_cache_short",
422 422 "repository_repo_ui")).all()
423 423
424 424 hg_ui = ret
425 425 for ui_ in hg_ui:
426 426 if ui_.ui_active:
427 427 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
428 428 ui_.ui_key, ui_.ui_value)
429 429 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
430 430
431 431 return baseui
432 432
433 433 #==========================================================================
434 434 # SCM CACHE INSTANCE
435 435 #==========================================================================
436 436
437 437 @property
438 438 def invalidate(self):
439 439 """
440 440 Returns Invalidation object if this repo should be invalidated
441 441 None otherwise. `cache_active = False` means that this cache
442 442 state is not valid and needs to be invalidated
443 443 """
444 444 return Session.query(CacheInvalidation)\
445 445 .filter(CacheInvalidation.cache_key == self.repo_name)\
446 446 .filter(CacheInvalidation.cache_active == False)\
447 447 .scalar()
448 448
449 449 @property
450 450 def set_invalidate(self):
451 451 """
452 452 set a cache for invalidation for this instance
453 453 """
454 454 inv = Session.query(CacheInvalidation)\
455 455 .filter(CacheInvalidation.cache_key == self.repo_name)\
456 456 .scalar()
457 457
458 458 if inv is None:
459 459 inv = CacheInvalidation(self.repo_name)
460 460 inv.cache_active = True
461 461 Session.add(inv)
462 462 Session.commit()
463 463
464 464 @property
465 465 def scm_instance(self):
466 466 return self.__get_instance()
467 467
468 468 @property
469 469 def scm_instance_cached(self):
470 470 @cache_region('long_term')
471 471 def _c(repo_name):
472 472 return self.__get_instance()
473 473
474 474 inv = self.invalidate
475 475 if inv:
476 476 region_invalidate(_c, None, self.repo_name)
477 477 #update our cache
478 478 inv.cache_key.cache_active = True
479 479 Session.add(inv)
480 480 Session.commit()
481 481
482 return _c(self.repo_name)
482 # TODO: remove this trick when beaker 1.6 is released
483 # and have fixed this issue
484 rn = safe_str(self.repo_name)
485
486 return _c(rn)
483 487
484 488 def __get_instance(self):
485 489
486 490 repo_full_path = self.repo_full_path
487 491
488 492 try:
489 493 alias = get_scm(repo_full_path)[0]
490 494 log.debug('Creating instance of %s repository', alias)
491 495 backend = get_backend(alias)
492 496 except VCSError:
493 497 log.error(traceback.format_exc())
494 498 log.error('Perhaps this repository is in db and not in '
495 499 'filesystem run rescan repositories with '
496 500 '"destroy old data " option from admin panel')
497 501 return
498 502
499 503 if alias == 'hg':
500 repo = backend(repo_full_path, create=False,
504
505 repo = backend(safe_str(repo_full_path), create=False,
501 506 baseui=self._ui)
502 507 #skip hidden web repository
503 508 if repo._get_hidden():
504 509 return
505 510 else:
506 511 repo = backend(repo_full_path, create=False)
507 512
508 513 return repo
509 514
510 515
511 516 class Group(Base, BaseModel):
512 517 __tablename__ = 'groups'
513 518 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
514 519 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
515 520 __mapper_args__ = {'order_by':'group_name'}
516 521
517 522 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
518 523 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
519 524 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
520 525 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
521 526
522 527 parent_group = relationship('Group', remote_side=group_id)
523 528
524 529
525 530 def __init__(self, group_name='', parent_group=None):
526 531 self.group_name = group_name
527 532 self.parent_group = parent_group
528 533
529 534 def __repr__(self):
530 535 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
531 536 self.group_name)
532 537
533 538 @classmethod
534 539 def url_sep(cls):
535 540 return '/'
536 541
537 542 @property
538 543 def parents(self):
539 544 parents_recursion_limit = 5
540 545 groups = []
541 546 if self.parent_group is None:
542 547 return groups
543 548 cur_gr = self.parent_group
544 549 groups.insert(0, cur_gr)
545 550 cnt = 0
546 551 while 1:
547 552 cnt += 1
548 553 gr = getattr(cur_gr, 'parent_group', None)
549 554 cur_gr = cur_gr.parent_group
550 555 if gr is None:
551 556 break
552 557 if cnt == parents_recursion_limit:
553 558 # this will prevent accidental infinit loops
554 559 log.error('group nested more than %s' %
555 560 parents_recursion_limit)
556 561 break
557 562
558 563 groups.insert(0, gr)
559 564 return groups
560 565
561 566 @property
562 567 def children(self):
563 568 return Session.query(Group).filter(Group.parent_group == self)
564 569
565 570 @property
566 571 def full_path(self):
567 572 return Group.url_sep().join([g.group_name for g in self.parents] +
568 573 [self.group_name])
569 574
570 575 @property
571 576 def repositories(self):
572 577 return Session.query(Repository).filter(Repository.group == self)
573 578
574 579 @property
575 580 def repositories_recursive_count(self):
576 581 cnt = self.repositories.count()
577 582
578 583 def children_count(group):
579 584 cnt = 0
580 585 for child in group.children:
581 586 cnt += child.repositories.count()
582 587 cnt += children_count(child)
583 588 return cnt
584 589
585 590 return cnt + children_count(self)
586 591
587 592 class Permission(Base, BaseModel):
588 593 __tablename__ = 'permissions'
589 594 __table_args__ = {'extend_existing':True}
590 595 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 596 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
592 597 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
593 598
594 599 def __repr__(self):
595 600 return "<%s('%s:%s')>" % (self.__class__.__name__,
596 601 self.permission_id, self.permission_name)
597 602
598 603 @classmethod
599 604 def get_by_key(cls, key):
600 605 return Session.query(cls).filter(cls.permission_name == key).scalar()
601 606
602 607 class RepoToPerm(Base, BaseModel):
603 608 __tablename__ = 'repo_to_perm'
604 609 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
605 610 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
606 611 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
607 612 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
608 613 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
609 614
610 615 user = relationship('User')
611 616 permission = relationship('Permission')
612 617 repository = relationship('Repository')
613 618
614 619 class UserToPerm(Base, BaseModel):
615 620 __tablename__ = 'user_to_perm'
616 621 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
617 622 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
618 623 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
619 624 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
620 625
621 626 user = relationship('User')
622 627 permission = relationship('Permission')
623 628
624 629 @classmethod
625 630 def has_perm(cls, user_id, perm):
626 631 if not isinstance(perm, Permission):
627 632 raise Exception('perm needs to be an instance of Permission class')
628 633
629 634 return Session.query(cls).filter(cls.user_id == user_id)\
630 635 .filter(cls.permission == perm).scalar() is not None
631 636
632 637 @classmethod
633 638 def grant_perm(cls, user_id, perm):
634 639 if not isinstance(perm, Permission):
635 640 raise Exception('perm needs to be an instance of Permission class')
636 641
637 642 new = cls()
638 643 new.user_id = user_id
639 644 new.permission = perm
640 645 try:
641 646 Session.add(new)
642 647 Session.commit()
643 648 except:
644 649 Session.rollback()
645 650
646 651
647 652 @classmethod
648 653 def revoke_perm(cls, user_id, perm):
649 654 if not isinstance(perm, Permission):
650 655 raise Exception('perm needs to be an instance of Permission class')
651 656
652 657 try:
653 658 Session.query(cls).filter(cls.user_id == user_id)\
654 659 .filter(cls.permission == perm).delete()
655 660 Session.commit()
656 661 except:
657 662 Session.rollback()
658 663
659 664 class UsersGroupRepoToPerm(Base, BaseModel):
660 665 __tablename__ = 'users_group_repo_to_perm'
661 666 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
662 667 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
663 668 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
664 669 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
665 670 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
666 671
667 672 users_group = relationship('UsersGroup')
668 673 permission = relationship('Permission')
669 674 repository = relationship('Repository')
670 675
671 676
672 677 class UsersGroupToPerm(Base, BaseModel):
673 678 __tablename__ = 'users_group_to_perm'
674 679 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
675 680 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
676 681 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
677 682
678 683 users_group = relationship('UsersGroup')
679 684 permission = relationship('Permission')
680 685
681 686
682 687 @classmethod
683 688 def has_perm(cls, users_group_id, perm):
684 689 if not isinstance(perm, Permission):
685 690 raise Exception('perm needs to be an instance of Permission class')
686 691
687 692 return Session.query(cls).filter(cls.users_group_id ==
688 693 users_group_id)\
689 694 .filter(cls.permission == perm)\
690 695 .scalar() is not None
691 696
692 697 @classmethod
693 698 def grant_perm(cls, users_group_id, perm):
694 699 if not isinstance(perm, Permission):
695 700 raise Exception('perm needs to be an instance of Permission class')
696 701
697 702 new = cls()
698 703 new.users_group_id = users_group_id
699 704 new.permission = perm
700 705 try:
701 706 Session.add(new)
702 707 Session.commit()
703 708 except:
704 709 Session.rollback()
705 710
706 711
707 712 @classmethod
708 713 def revoke_perm(cls, users_group_id, perm):
709 714 if not isinstance(perm, Permission):
710 715 raise Exception('perm needs to be an instance of Permission class')
711 716
712 717 try:
713 718 Session.query(cls).filter(cls.users_group_id == users_group_id)\
714 719 .filter(cls.permission == perm).delete()
715 720 Session.commit()
716 721 except:
717 722 Session.rollback()
718 723
719 724
720 725 class GroupToPerm(Base, BaseModel):
721 726 __tablename__ = 'group_to_perm'
722 727 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
723 728
724 729 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
725 730 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
726 731 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
727 732 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
728 733
729 734 user = relationship('User')
730 735 permission = relationship('Permission')
731 736 group = relationship('Group')
732 737
733 738 class Statistics(Base, BaseModel):
734 739 __tablename__ = 'statistics'
735 740 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
736 741 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
737 742 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
738 743 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
739 744 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
740 745 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
741 746 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
742 747
743 748 repository = relationship('Repository', single_parent=True)
744 749
745 750 class UserFollowing(Base, BaseModel):
746 751 __tablename__ = 'user_followings'
747 752 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
748 753 UniqueConstraint('user_id', 'follows_user_id')
749 754 , {'extend_existing':True})
750 755
751 756 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
752 757 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
753 758 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
754 759 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
755 760 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
756 761
757 762 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
758 763
759 764 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
760 765 follows_repository = relationship('Repository', order_by='Repository.repo_name')
761 766
762 767
763 768 @classmethod
764 769 def get_repo_followers(cls, repo_id):
765 770 return Session.query(cls).filter(cls.follows_repo_id == repo_id)
766 771
767 772 class CacheInvalidation(Base, BaseModel):
768 773 __tablename__ = 'cache_invalidation'
769 774 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
770 775 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
771 776 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
772 777 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
773 778 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
774 779
775 780
776 781 def __init__(self, cache_key, cache_args=''):
777 782 self.cache_key = cache_key
778 783 self.cache_args = cache_args
779 784 self.cache_active = False
780 785
781 786 def __repr__(self):
782 787 return "<%s('%s:%s')>" % (self.__class__.__name__,
783 788 self.cache_id, self.cache_key)
784 789
785 790 class DbMigrateVersion(Base, BaseModel):
786 791 __tablename__ = 'db_migrate_version'
787 792 __table_args__ = {'extend_existing':True}
788 793 repository_id = Column('repository_id', String(250), primary_key=True)
789 794 repository_path = Column('repository_path', Text)
790 795 version = Column('version', Integer)
@@ -1,355 +1,358 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.repo
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repository model for rhodecode
7 7
8 8 :created_on: Jun 5, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import shutil
27 27 import logging
28 28 import traceback
29 29 from datetime import datetime
30 30
31 31 from sqlalchemy.orm import joinedload, make_transient
32 32
33 33 from vcs.utils.lazy import LazyProperty
34 34 from vcs.backends import get_backend
35 35
36 from rhodecode.lib import safe_str
37
36 38 from rhodecode.model import BaseModel
37 39 from rhodecode.model.caching_query import FromCache
38 40 from rhodecode.model.db import Repository, RepoToPerm, User, Permission, \
39 41 Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, Group
40 42 from rhodecode.model.user import UserModel
41 43
42 44 log = logging.getLogger(__name__)
43 45
44 46
45 47 class RepoModel(BaseModel):
46 48
47 49 @LazyProperty
48 50 def repos_path(self):
49 51 """Get's the repositories root path from database
50 52 """
51 53
52 54 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
53 55 return q.ui_value
54 56
55 57 def get(self, repo_id, cache=False):
56 58 repo = self.sa.query(Repository)\
57 59 .filter(Repository.repo_id == repo_id)
58 60
59 61 if cache:
60 62 repo = repo.options(FromCache("sql_cache_short",
61 63 "get_repo_%s" % repo_id))
62 64 return repo.scalar()
63 65
64 66 def get_by_repo_name(self, repo_name, cache=False):
65 67 repo = self.sa.query(Repository)\
66 68 .filter(Repository.repo_name == repo_name)
67 69
68 70 if cache:
69 71 repo = repo.options(FromCache("sql_cache_short",
70 72 "get_repo_%s" % repo_name))
71 73 return repo.scalar()
72 74
73 75
74 76 def get_users_js(self):
75 77
76 78 users = self.sa.query(User).filter(User.active == True).all()
77 79 u_tmpl = '''{id:%s, fname:"%s", lname:"%s", nname:"%s"},'''
78 80 users_array = '[%s]' % '\n'.join([u_tmpl % (u.user_id, u.name,
79 81 u.lastname, u.username)
80 82 for u in users])
81 83 return users_array
82 84
83 85 def get_users_groups_js(self):
84 86 users_groups = self.sa.query(UsersGroup)\
85 87 .filter(UsersGroup.users_group_active == True).all()
86 88
87 89 g_tmpl = '''{id:%s, grname:"%s",grmembers:"%s"},'''
88 90
89 91 users_groups_array = '[%s]' % '\n'.join([g_tmpl % \
90 92 (gr.users_group_id, gr.users_group_name,
91 93 len(gr.members))
92 94 for gr in users_groups])
93 95 return users_groups_array
94 96
95 97 def update(self, repo_name, form_data):
96 98 try:
97 99 cur_repo = self.get_by_repo_name(repo_name, cache=False)
98 100
99 101 #update permissions
100 102 for member, perm, member_type in form_data['perms_updates']:
101 103 if member_type == 'user':
102 104 r2p = self.sa.query(RepoToPerm)\
103 105 .filter(RepoToPerm.user == User.by_username(member))\
104 106 .filter(RepoToPerm.repository == cur_repo)\
105 107 .one()
106 108
107 109 r2p.permission = self.sa.query(Permission)\
108 110 .filter(Permission.permission_name ==
109 111 perm).scalar()
110 112 self.sa.add(r2p)
111 113 else:
112 114 g2p = self.sa.query(UsersGroupRepoToPerm)\
113 115 .filter(UsersGroupRepoToPerm.users_group ==
114 116 UsersGroup.get_by_group_name(member))\
115 117 .filter(UsersGroupRepoToPerm.repository ==
116 118 cur_repo).one()
117 119
118 120 g2p.permission = self.sa.query(Permission)\
119 121 .filter(Permission.permission_name ==
120 122 perm).scalar()
121 123 self.sa.add(g2p)
122 124
123 125 #set new permissions
124 126 for member, perm, member_type in form_data['perms_new']:
125 127 if member_type == 'user':
126 128 r2p = RepoToPerm()
127 129 r2p.repository = cur_repo
128 130 r2p.user = User.by_username(member)
129 131
130 132 r2p.permission = self.sa.query(Permission)\
131 133 .filter(Permission.
132 134 permission_name == perm)\
133 135 .scalar()
134 136 self.sa.add(r2p)
135 137 else:
136 138 g2p = UsersGroupRepoToPerm()
137 139 g2p.repository = cur_repo
138 140 g2p.users_group = UsersGroup.get_by_group_name(member)
139 141 g2p.permission = self.sa.query(Permission)\
140 142 .filter(Permission.
141 143 permission_name == perm)\
142 144 .scalar()
143 145 self.sa.add(g2p)
144 146
145 147 #update current repo
146 148 for k, v in form_data.items():
147 149 if k == 'user':
148 150 cur_repo.user = User.by_username(v)
149 151 elif k == 'repo_name':
150 152 cur_repo.repo_name = form_data['repo_name_full']
151 153 elif k == 'repo_group':
152 154 cur_repo.group_id = v
153 155
154 156 else:
155 157 setattr(cur_repo, k, v)
156 158
157 159 self.sa.add(cur_repo)
158 160
159 161 if repo_name != form_data['repo_name_full']:
160 162 # rename repository
161 163 self.__rename_repo(old=repo_name,
162 164 new=form_data['repo_name_full'])
163 165
164 166 self.sa.commit()
165 167 except:
166 168 log.error(traceback.format_exc())
167 169 self.sa.rollback()
168 170 raise
169 171
170 172 def create(self, form_data, cur_user, just_db=False, fork=False):
171 173
172 174 try:
173 175 if fork:
174 #force str since hg doesn't go with unicode
175 repo_name = str(form_data['fork_name'])
176 org_name = str(form_data['repo_name'])
177 org_full_name = org_name#str(form_data['fork_name_full'])
176 repo_name = form_data['fork_name']
177 org_name = form_data['repo_name']
178 org_full_name = org_name
178 179
179 180 else:
180 org_name = repo_name = str(form_data['repo_name'])
181 org_name = repo_name = form_data['repo_name']
181 182 repo_name_full = form_data['repo_name_full']
182 183
183 184 new_repo = Repository()
184 185 new_repo.enable_statistics = False
185 186 for k, v in form_data.items():
186 187 if k == 'repo_name':
187 188 if fork:
188 189 v = repo_name
189 190 else:
190 191 v = repo_name_full
191 192 if k == 'repo_group':
192 193 k = 'group_id'
193 194
194 195 setattr(new_repo, k, v)
195 196
196 197 if fork:
197 198 parent_repo = self.sa.query(Repository)\
198 199 .filter(Repository.repo_name == org_full_name).one()
199 200 new_repo.fork = parent_repo
200 201
201 202 new_repo.user_id = cur_user.user_id
202 203 self.sa.add(new_repo)
203 204
204 205 #create default permission
205 206 repo_to_perm = RepoToPerm()
206 207 default = 'repository.read'
207 208 for p in UserModel(self.sa).get_by_username('default',
208 209 cache=False).user_perms:
209 210 if p.permission.permission_name.startswith('repository.'):
210 211 default = p.permission.permission_name
211 212 break
212 213
213 214 default_perm = 'repository.none' if form_data['private'] else default
214 215
215 216 repo_to_perm.permission_id = self.sa.query(Permission)\
216 217 .filter(Permission.permission_name == default_perm)\
217 218 .one().permission_id
218 219
219 220 repo_to_perm.repository = new_repo
220 221 repo_to_perm.user_id = UserModel(self.sa)\
221 222 .get_by_username('default', cache=False).user_id
222 223
223 224 self.sa.add(repo_to_perm)
224 225
225 226 if not just_db:
226 227 self.__create_repo(repo_name, form_data['repo_type'],
227 228 form_data['repo_group'],
228 229 form_data['clone_uri'])
229 230
230 231 self.sa.commit()
231 232
232 233 #now automatically start following this repository as owner
233 234 from rhodecode.model.scm import ScmModel
234 235 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
235 236 cur_user.user_id)
236 237
237 238 except:
238 239 log.error(traceback.format_exc())
239 240 self.sa.rollback()
240 241 raise
241 242
242 243 def create_fork(self, form_data, cur_user):
243 244 from rhodecode.lib.celerylib import tasks, run_task
244 245 run_task(tasks.create_repo_fork, form_data, cur_user)
245 246
246 247 def delete(self, repo):
247 248 try:
248 249 self.sa.delete(repo)
249 250 self.__delete_repo(repo)
250 251 self.sa.commit()
251 252 except:
252 253 log.error(traceback.format_exc())
253 254 self.sa.rollback()
254 255 raise
255 256
256 257 def delete_perm_user(self, form_data, repo_name):
257 258 try:
258 259 self.sa.query(RepoToPerm)\
259 260 .filter(RepoToPerm.repository \
260 261 == self.get_by_repo_name(repo_name))\
261 262 .filter(RepoToPerm.user_id == form_data['user_id']).delete()
262 263 self.sa.commit()
263 264 except:
264 265 log.error(traceback.format_exc())
265 266 self.sa.rollback()
266 267 raise
267 268
268 269 def delete_perm_users_group(self, form_data, repo_name):
269 270 try:
270 271 self.sa.query(UsersGroupRepoToPerm)\
271 272 .filter(UsersGroupRepoToPerm.repository \
272 273 == self.get_by_repo_name(repo_name))\
273 274 .filter(UsersGroupRepoToPerm.users_group_id \
274 275 == form_data['users_group_id']).delete()
275 276 self.sa.commit()
276 277 except:
277 278 log.error(traceback.format_exc())
278 279 self.sa.rollback()
279 280 raise
280 281
281 282 def delete_stats(self, repo_name):
282 283 try:
283 284 self.sa.query(Statistics)\
284 285 .filter(Statistics.repository == \
285 286 self.get_by_repo_name(repo_name)).delete()
286 287 self.sa.commit()
287 288 except:
288 289 log.error(traceback.format_exc())
289 290 self.sa.rollback()
290 291 raise
291 292
292 293 def __create_repo(self, repo_name, alias, new_parent_id, clone_uri=False):
293 294 """
294 295 makes repository on filesystem. It's group aware means it'll create
295 296 a repository within a group, and alter the paths accordingly of
296 297 group location
297 298
298 299 :param repo_name:
299 300 :param alias:
300 301 :param parent_id:
301 302 :param clone_uri:
302 303 """
303 304 from rhodecode.lib.utils import check_repo
304 305
305
306 306 if new_parent_id:
307 307 paths = Group.get(new_parent_id).full_path.split(Group.url_sep())
308 308 new_parent_path = os.sep.join(paths)
309 309 else:
310 310 new_parent_path = ''
311 311
312 repo_path = os.path.join(self.repos_path, new_parent_path, repo_name)
312 repo_path = os.path.join(*map(lambda x:safe_str(x),
313 [self.repos_path, new_parent_path, repo_name]))
313 314
314 if check_repo(repo_name, self.repos_path):
315 if check_repo(repo_path, self.repos_path):
315 316 log.info('creating repo %s in %s @ %s', repo_name, repo_path,
316 clone_uri)
317 clone_uri)
317 318 backend = get_backend(alias)
319
318 320 backend(repo_path, create=True, src_url=clone_uri)
319 321
322
320 323 def __rename_repo(self, old, new):
321 324 """
322 325 renames repository on filesystem
323 326
324 327 :param old: old name
325 328 :param new: new name
326 329 """
327 330 log.info('renaming repo from %s to %s', old, new)
328 331
329 332 old_path = os.path.join(self.repos_path, old)
330 333 new_path = os.path.join(self.repos_path, new)
331 334 if os.path.isdir(new_path):
332 335 raise Exception('Was trying to rename to already existing dir %s',
333 336 new_path)
334 337 shutil.move(old_path, new_path)
335 338
336 339 def __delete_repo(self, repo):
337 340 """
338 341 removes repo from filesystem, the removal is acctually made by
339 342 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
340 343 repository is no longer valid for rhodecode, can be undeleted later on
341 344 by reverting the renames on this repository
342 345
343 346 :param repo: repo object
344 347 """
345 348 rm_path = os.path.join(self.repos_path, repo.repo_name)
346 349 log.info("Removing %s", rm_path)
347 350 #disable hg/git
348 351 alias = repo.repo_type
349 352 shutil.move(os.path.join(rm_path, '.%s' % alias),
350 353 os.path.join(rm_path, 'rm__.%s' % alias))
351 354 #disable repo
352 355 shutil.move(rm_path, os.path.join(self.repos_path, 'rm__%s__%s' \
353 356 % (datetime.today()\
354 357 .strftime('%Y%m%d_%H%M%S_%f'),
355 358 repo.repo_name)))
@@ -1,416 +1,413 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import time
27 27 import traceback
28 28 import logging
29 29
30 from mercurial import ui
31
32 30 from sqlalchemy.exc import DatabaseError
33 from sqlalchemy.orm import make_transient
34
35 from beaker.cache import cache_region, region_invalidate
36 31
37 32 from vcs import get_backend
38 33 from vcs.utils.helpers import get_scm
39 34 from vcs.exceptions import RepositoryError, VCSError
40 35 from vcs.utils.lazy import LazyProperty
41 36 from vcs.nodes import FileNode
42 37
43 38 from rhodecode import BACKENDS
44 39 from rhodecode.lib import helpers as h
40 from rhodecode.lib import safe_str
45 41 from rhodecode.lib.auth import HasRepoPermissionAny
46 42 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
47 43 action_logger
48 44 from rhodecode.model import BaseModel
49 45 from rhodecode.model.user import UserModel
50 from rhodecode.model.repo import RepoModel
51 46 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
52 47 UserFollowing, UserLog
53 from rhodecode.model.caching_query import FromCache
54 48
55 49 log = logging.getLogger(__name__)
56 50
57 51
58 52 class UserTemp(object):
59 53 def __init__(self, user_id):
60 54 self.user_id = user_id
61 55
62 56 def __repr__(self):
63 57 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64 58
65 59
66 60 class RepoTemp(object):
67 61 def __init__(self, repo_id):
68 62 self.repo_id = repo_id
69 63
70 64 def __repr__(self):
71 65 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72 66
73 67 class CachedRepoList(object):
74 68
75 69 def __init__(self, db_repo_list, invalidation_list, repos_path,
76 70 order_by=None):
77 71 self.db_repo_list = db_repo_list
78 72 self.invalidation_list = invalidation_list
79 73 self.repos_path = repos_path
80 74 self.order_by = order_by
81 75 self.reversed = (order_by or '').startswith('-')
82 76
83 77 def __len__(self):
84 78 return len(self.db_repo_list)
85 79
86 80 def __repr__(self):
87 81 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
88 82
89 83 def __iter__(self):
90 84 for db_repo in self.db_repo_list:
91 85 dbr = db_repo
92 86
93 87 # invalidate the repo cache if needed before getting the
94 88 # scm instance
95 89
96 90 scm_invalidate = False
97 91 if self.invalidation_list is not None:
98 92 scm_invalidate = dbr.repo_name in self.invalidation_list
99 93
100 94 if scm_invalidate:
101 95 log.info('invalidating cache for repository %s',
102 96 dbr.repo_name)
103 97 db_repo.set_invalidate
104 98
105 99 scmr = db_repo.scm_instance_cached
106 100
107 101 #check permission at this level
108 102 if not HasRepoPermissionAny('repository.read',
109 103 'repository.write',
110 104 'repository.admin')(dbr.repo_name,
111 105 'get repo check'):
112 106 continue
113 107
114 108
115 109 if scmr is None:
116 110 log.error('%s this repository is present in database but it '
117 111 'cannot be created as an scm instance',
118 112 dbr.repo_name)
119 113 continue
120 114
121 115
122 116 last_change = scmr.last_change
123 117 tip = h.get_changeset_safe(scmr, 'tip')
124 118
125 119 tmp_d = {}
126 120 tmp_d['name'] = dbr.repo_name
127 121 tmp_d['name_sort'] = tmp_d['name'].lower()
128 122 tmp_d['description'] = dbr.description
129 123 tmp_d['description_sort'] = tmp_d['description']
130 124 tmp_d['last_change'] = last_change
131 125 tmp_d['last_change_sort'] = time.mktime(last_change \
132 126 .timetuple())
133 127 tmp_d['tip'] = tip.raw_id
134 128 tmp_d['tip_sort'] = tip.revision
135 129 tmp_d['rev'] = tip.revision
136 130 tmp_d['contact'] = dbr.user.full_contact
137 131 tmp_d['contact_sort'] = tmp_d['contact']
138 132 tmp_d['owner_sort'] = tmp_d['contact']
139 133 tmp_d['repo_archives'] = list(scmr._get_archives())
140 134 tmp_d['last_msg'] = tip.message
141 135 tmp_d['repo'] = scmr
142 136 tmp_d['dbrepo'] = dbr.get_dict()
143 137 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork \
144 138 else {}
145 139 yield tmp_d
146 140
147 141 class ScmModel(BaseModel):
148 142 """Generic Scm Model
149 143 """
150 144
151 145 @LazyProperty
152 146 def repos_path(self):
153 147 """Get's the repositories root path from database
154 148 """
155 149
156 150 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
157 151
158 152 return q.ui_value
159 153
160 154 def repo_scan(self, repos_path=None):
161 155 """Listing of repositories in given path. This path should not be a
162 156 repository itself. Return a dictionary of repository objects
163 157
164 158 :param repos_path: path to directory containing repositories
165 159 """
166 160
167 161 log.info('scanning for repositories in %s', repos_path)
168 162
169 163 if repos_path is None:
170 164 repos_path = self.repos_path
171 165
172 166 baseui = make_ui('db')
173 167 repos_list = {}
174 168
175 169 for name, path in get_filesystem_repos(repos_path, recursive=True):
176 170 try:
177 171 if name in repos_list:
178 172 raise RepositoryError('Duplicate repository name %s '
179 173 'found in %s' % (name, path))
180 174 else:
181 175
182 176 klass = get_backend(path[0])
183 177
184 178 if path[0] == 'hg' and path[0] in BACKENDS.keys():
185 repos_list[name] = klass(path[1], baseui=baseui)
179
180 # for mercurial we need to have an str path
181 repos_list[name] = klass(safe_str(path[1]),
182 baseui=baseui)
186 183
187 184 if path[0] == 'git' and path[0] in BACKENDS.keys():
188 185 repos_list[name] = klass(path[1])
189 186 except OSError:
190 187 continue
191 188
192 189 return repos_list
193 190
194 191 def get_repos(self, all_repos=None, sort_key=None):
195 192 """
196 193 Get all repos from db and for each repo create it's
197 194 backend instance and fill that backed with information from database
198 195
199 196 :param all_repos: list of repository names as strings
200 197 give specific repositories list, good for filtering
201 198 """
202 199 if all_repos is None:
203 200 all_repos = self.sa.query(Repository)\
204 201 .filter(Repository.group_id == None)\
205 202 .order_by(Repository.repo_name).all()
206 203
207 204 #get the repositories that should be invalidated
208 205 invalidation_list = [str(x.cache_key) for x in \
209 206 self.sa.query(CacheInvalidation.cache_key)\
210 207 .filter(CacheInvalidation.cache_active == False)\
211 208 .all()]
212 209
213 210 repo_iter = CachedRepoList(all_repos, invalidation_list,
214 211 repos_path=self.repos_path,
215 212 order_by=sort_key)
216 213
217 214 return repo_iter
218 215
219 216 def mark_for_invalidation(self, repo_name):
220 217 """Puts cache invalidation task into db for
221 218 further global cache invalidation
222 219
223 220 :param repo_name: this repo that should invalidation take place
224 221 """
225 222
226 223 log.debug('marking %s for invalidation', repo_name)
227 224 cache = self.sa.query(CacheInvalidation)\
228 225 .filter(CacheInvalidation.cache_key == repo_name).scalar()
229 226
230 227 if cache:
231 228 #mark this cache as inactive
232 229 cache.cache_active = False
233 230 else:
234 231 log.debug('cache key not found in invalidation db -> creating one')
235 232 cache = CacheInvalidation(repo_name)
236 233
237 234 try:
238 235 self.sa.add(cache)
239 236 self.sa.commit()
240 237 except (DatabaseError,):
241 238 log.error(traceback.format_exc())
242 239 self.sa.rollback()
243 240
244 241 def toggle_following_repo(self, follow_repo_id, user_id):
245 242
246 243 f = self.sa.query(UserFollowing)\
247 244 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
248 245 .filter(UserFollowing.user_id == user_id).scalar()
249 246
250 247 if f is not None:
251 248
252 249 try:
253 250 self.sa.delete(f)
254 251 self.sa.commit()
255 252 action_logger(UserTemp(user_id),
256 253 'stopped_following_repo',
257 254 RepoTemp(follow_repo_id))
258 255 return
259 256 except:
260 257 log.error(traceback.format_exc())
261 258 self.sa.rollback()
262 259 raise
263 260
264 261 try:
265 262 f = UserFollowing()
266 263 f.user_id = user_id
267 264 f.follows_repo_id = follow_repo_id
268 265 self.sa.add(f)
269 266 self.sa.commit()
270 267 action_logger(UserTemp(user_id),
271 268 'started_following_repo',
272 269 RepoTemp(follow_repo_id))
273 270 except:
274 271 log.error(traceback.format_exc())
275 272 self.sa.rollback()
276 273 raise
277 274
278 275 def toggle_following_user(self, follow_user_id, user_id):
279 276 f = self.sa.query(UserFollowing)\
280 277 .filter(UserFollowing.follows_user_id == follow_user_id)\
281 278 .filter(UserFollowing.user_id == user_id).scalar()
282 279
283 280 if f is not None:
284 281 try:
285 282 self.sa.delete(f)
286 283 self.sa.commit()
287 284 return
288 285 except:
289 286 log.error(traceback.format_exc())
290 287 self.sa.rollback()
291 288 raise
292 289
293 290 try:
294 291 f = UserFollowing()
295 292 f.user_id = user_id
296 293 f.follows_user_id = follow_user_id
297 294 self.sa.add(f)
298 295 self.sa.commit()
299 296 except:
300 297 log.error(traceback.format_exc())
301 298 self.sa.rollback()
302 299 raise
303 300
304 301 def is_following_repo(self, repo_name, user_id, cache=False):
305 302 r = self.sa.query(Repository)\
306 303 .filter(Repository.repo_name == repo_name).scalar()
307 304
308 305 f = self.sa.query(UserFollowing)\
309 306 .filter(UserFollowing.follows_repository == r)\
310 307 .filter(UserFollowing.user_id == user_id).scalar()
311 308
312 309 return f is not None
313 310
314 311 def is_following_user(self, username, user_id, cache=False):
315 312 u = UserModel(self.sa).get_by_username(username)
316 313
317 314 f = self.sa.query(UserFollowing)\
318 315 .filter(UserFollowing.follows_user == u)\
319 316 .filter(UserFollowing.user_id == user_id).scalar()
320 317
321 318 return f is not None
322 319
323 320 def get_followers(self, repo_id):
324 321 if not isinstance(repo_id, int):
325 322 repo_id = getattr(Repository.by_repo_name(repo_id), 'repo_id')
326 323
327 324 return self.sa.query(UserFollowing)\
328 325 .filter(UserFollowing.follows_repo_id == repo_id).count()
329 326
330 327 def get_forks(self, repo_id):
331 328 if not isinstance(repo_id, int):
332 329 repo_id = getattr(Repository.by_repo_name(repo_id), 'repo_id')
333 330
334 331 return self.sa.query(Repository)\
335 332 .filter(Repository.fork_id == repo_id).count()
336 333
337 334 def pull_changes(self, repo_name, username):
338 335 dbrepo = Repository.by_repo_name(repo_name)
339 336 repo = dbrepo.scm_instance
340 337 try:
341 338 extras = {'ip': '',
342 339 'username': username,
343 340 'action': 'push_remote',
344 341 'repository': repo_name}
345 342
346 343 #inject ui extra param to log this action via push logger
347 344 for k, v in extras.items():
348 345 repo._repo.ui.setconfig('rhodecode_extras', k, v)
349 346
350 347 repo.pull(dbrepo.clone_uri)
351 348 self.mark_for_invalidation(repo_name)
352 349 except:
353 350 log.error(traceback.format_exc())
354 351 raise
355 352
356 353
357 354 def commit_change(self, repo, repo_name, cs, user, author, message, content,
358 355 f_path):
359 356
360 357 if repo.alias == 'hg':
361 358 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
362 359 elif repo.alias == 'git':
363 360 from vcs.backends.git import GitInMemoryChangeset as IMC
364 361
365 362 # decoding here will force that we have proper encoded values
366 363 # in any other case this will throw exceptions and deny commit
367 content = content.encode('utf8')
368 message = message.encode('utf8')
369 path = f_path.encode('utf8')
370 author = author.encode('utf8')
364 content = safe_str(content)
365 message = safe_str(message)
366 path = safe_str(f_path)
367 author = safe_str(author)
371 368 m = IMC(repo)
372 369 m.change(FileNode(path, content))
373 370 tip = m.commit(message=message,
374 371 author=author,
375 372 parents=[cs], branch=cs.branch)
376 373
377 374 new_cs = tip.short_id
378 375 action = 'push_local:%s' % new_cs
379 376
380 377 action_logger(user, action, repo_name)
381 378
382 379 self.mark_for_invalidation(repo_name)
383 380
384 381
385 382 def get_unread_journal(self):
386 383 return self.sa.query(UserLog).count()
387 384
388 385 def _should_invalidate(self, repo_name):
389 386 """Looks up database for invalidation signals for this repo_name
390 387
391 388 :param repo_name:
392 389 """
393 390
394 391 ret = self.sa.query(CacheInvalidation)\
395 392 .filter(CacheInvalidation.cache_key == repo_name)\
396 393 .filter(CacheInvalidation.cache_active == False)\
397 394 .scalar()
398 395
399 396 return ret
400 397
401 398 def _mark_invalidated(self, cache_key):
402 399 """ Marks all occurrences of cache to invalidation as already
403 400 invalidated
404 401
405 402 :param cache_key:
406 403 """
407 404
408 405 if cache_key:
409 406 log.debug('marking %s as already invalidated', cache_key)
410 407 try:
411 408 cache_key.cache_active = True
412 409 self.sa.add(cache_key)
413 410 self.sa.commit()
414 411 except (DatabaseError,):
415 412 log.error(traceback.format_exc())
416 413 self.sa.rollback()
@@ -1,35 +1,37 b''
1 1 from rhodecode.tests import *
2 2 import os
3 3 from nose.plugins.skip import SkipTest
4 4
5 5 class TestSearchController(TestController):
6 6
7 7 def test_index(self):
8 8 self.log_user()
9 9 response = self.app.get(url(controller='search', action='index'))
10 print response.body
11 assert 'class="small" id="q" name="q" type="text"' in response.body, 'Search box content error'
10
11 self.assertTrue('class="small" id="q" name="q" type="text"' in
12 response.body)
12 13 # Test response...
13 14
14 15 def test_empty_search(self):
15 16 if os.path.isdir(self.index_location):
16 17 raise SkipTest('skipped due to existing index')
17 18 else:
18 19 self.log_user()
19 response = self.app.get(url(controller='search', action='index'), {'q':HG_REPO})
20 assert 'There is no index to search in. Please run whoosh indexer' in response.body, 'No error message about empty index'
20 response = self.app.get(url(controller='search', action='index'),
21 {'q':HG_REPO})
22 self.assertTrue('There is no index to search in. '
23 'Please run whoosh indexer' in response.body)
21 24
22 25 def test_normal_search(self):
23 26 self.log_user()
24 response = self.app.get(url(controller='search', action='index'), {'q':'def repo'})
25 print response.body
26 assert '10 results' in response.body, 'no message about proper search results'
27 assert 'Permission denied' not in response.body, 'Wrong permissions settings for that repo and user'
28
27 response = self.app.get(url(controller='search', action='index'),
28 {'q':'def repo'})
29 self.assertTrue('10 results' in response.body)
30 self.assertTrue('Permission denied' not in response.body)
29 31
30 32 def test_repo_search(self):
31 33 self.log_user()
32 response = self.app.get(url(controller='search', action='index'), {'q':'repository:%s def test' % HG_REPO})
33 print response.body
34 assert '4 results' in response.body, 'no message about proper search results'
35 assert 'Permission denied' not in response.body, 'Wrong permissions settings for that repo and user'
34 response = self.app.get(url(controller='search', action='index'),
35 {'q':'repository:%s def test' % HG_REPO})
36 self.assertTrue('4 results' in response.body)
37 self.assertTrue('Permission denied' not in response.body)
General Comments 0
You need to be logged in to leave comments. Login now