##// END OF EJS Templates
normalize path using os.sep
marcink -
r1836:445861e1 beta
parent child Browse files
Show More
@@ -1,599 +1,599 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import paste
31 31 import beaker
32 32 import tarfile
33 33 import shutil
34 34 from os.path import abspath
35 35 from os.path import dirname as dn, join as jn
36 36
37 37 from paste.script.command import Command, BadCommand
38 38
39 39 from mercurial import ui, config
40 40
41 41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 42
43 43 from vcs import get_backend
44 44 from vcs.backends.base import BaseChangeset
45 45 from vcs.utils.lazy import LazyProperty
46 46 from vcs.utils.helpers import get_scm
47 47 from vcs.exceptions import VCSError
48 48
49 49 from rhodecode.lib.caching_query import FromCache
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
53 53 UserLog, RepoGroup, RhodeCodeSetting
54 54 from rhodecode.model.meta import Session
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def recursive_replace(str_, replace=' '):
60 60 """Recursive replace of given sign to just one instance
61 61
62 62 :param str_: given string
63 63 :param replace: char to find and replace multiple instances
64 64
65 65 Examples::
66 66 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
67 67 'Mighty-Mighty-Bo-sstones'
68 68 """
69 69
70 70 if str_.find(replace * 2) == -1:
71 71 return str_
72 72 else:
73 73 str_ = str_.replace(replace * 2, replace)
74 74 return recursive_replace(str_, replace)
75 75
76 76
77 77 def repo_name_slug(value):
78 78 """Return slug of name of repository
79 79 This function is called on each creation/modification
80 80 of repository to prevent bad names in repo
81 81 """
82 82
83 83 slug = remove_formatting(value)
84 84 slug = strip_tags(slug)
85 85
86 86 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
87 87 slug = slug.replace(c, '-')
88 88 slug = recursive_replace(slug, '-')
89 89 slug = collapse(slug, '-')
90 90 return slug
91 91
92 92
93 93 def get_repo_slug(request):
94 94 return request.environ['pylons.routes_dict'].get('repo_name')
95 95
96 96
97 97 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
98 98 """
99 99 Action logger for various actions made by users
100 100
101 101 :param user: user that made this action, can be a unique username string or
102 102 object containing user_id attribute
103 103 :param action: action to log, should be on of predefined unique actions for
104 104 easy translations
105 105 :param repo: string name of repository or object containing repo_id,
106 106 that action was made on
107 107 :param ipaddr: optional ip address from what the action was made
108 108 :param sa: optional sqlalchemy session
109 109
110 110 """
111 111
112 112 if not sa:
113 113 sa = meta.Session
114 114
115 115 try:
116 116 if hasattr(user, 'user_id'):
117 117 user_obj = user
118 118 elif isinstance(user, basestring):
119 119 user_obj = User.get_by_username(user)
120 120 else:
121 121 raise Exception('You have to provide user object or username')
122 122
123 123 if hasattr(repo, 'repo_id'):
124 124 repo_obj = Repository.get(repo.repo_id)
125 125 repo_name = repo_obj.repo_name
126 126 elif isinstance(repo, basestring):
127 127 repo_name = repo.lstrip('/')
128 128 repo_obj = Repository.get_by_repo_name(repo_name)
129 129 else:
130 130 raise Exception('You have to provide repository to action logger')
131 131
132 132 user_log = UserLog()
133 133 user_log.user_id = user_obj.user_id
134 134 user_log.action = action
135 135
136 136 user_log.repository_id = repo_obj.repo_id
137 137 user_log.repository_name = repo_name
138 138
139 139 user_log.action_date = datetime.datetime.now()
140 140 user_log.user_ip = ipaddr
141 141 sa.add(user_log)
142 142
143 143 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
144 144 if commit:
145 145 sa.commit()
146 146 except:
147 147 log.error(traceback.format_exc())
148 148 raise
149 149
150 150
151 151 def get_repos(path, recursive=False):
152 152 """
153 153 Scans given path for repos and return (name,(type,path)) tuple
154 154
155 :param path: path to scann for repositories
155 :param path: path to scan for repositories
156 156 :param recursive: recursive search and return names with subdirs in front
157 157 """
158 158
159 159 # remove ending slash for better results
160 path = path.rstrip('/')
160 path = path.rstrip(os.sep)
161 161
162 162 def _get_repos(p):
163 163 if not os.access(p, os.W_OK):
164 164 return
165 165 for dirpath in os.listdir(p):
166 166 if os.path.isfile(os.path.join(p, dirpath)):
167 167 continue
168 168 cur_path = os.path.join(p, dirpath)
169 169 try:
170 170 scm_info = get_scm(cur_path)
171 171 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
172 172 except VCSError:
173 173 if not recursive:
174 174 continue
175 175 #check if this dir containts other repos for recursive scan
176 176 rec_path = os.path.join(p, dirpath)
177 177 if os.path.isdir(rec_path):
178 178 for inner_scm in _get_repos(rec_path):
179 179 yield inner_scm
180 180
181 181 return _get_repos(path)
182 182
183 183
184 184 def is_valid_repo(repo_name, base_path):
185 185 """
186 186 Returns True if given path is a valid repository False otherwise
187 187 :param repo_name:
188 188 :param base_path:
189 189
190 190 :return True: if given path is a valid repository
191 191 """
192 192 full_path = os.path.join(base_path, repo_name)
193 193
194 194 try:
195 195 get_scm(full_path)
196 196 return True
197 197 except VCSError:
198 198 return False
199 199
200 200 def is_valid_repos_group(repos_group_name, base_path):
201 201 """
202 202 Returns True if given path is a repos group False otherwise
203 203
204 204 :param repo_name:
205 205 :param base_path:
206 206 """
207 207 full_path = os.path.join(base_path, repos_group_name)
208 208
209 209 # check if it's not a repo
210 210 if is_valid_repo(repos_group_name, base_path):
211 211 return False
212 212
213 213 # check if it's a valid path
214 214 if os.path.isdir(full_path):
215 215 return True
216 216
217 217 return False
218 218
219 219 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
220 220 while True:
221 221 ok = raw_input(prompt)
222 222 if ok in ('y', 'ye', 'yes'):
223 223 return True
224 224 if ok in ('n', 'no', 'nop', 'nope'):
225 225 return False
226 226 retries = retries - 1
227 227 if retries < 0:
228 228 raise IOError
229 229 print complaint
230 230
231 231 #propagated from mercurial documentation
232 232 ui_sections = ['alias', 'auth',
233 233 'decode/encode', 'defaults',
234 234 'diff', 'email',
235 235 'extensions', 'format',
236 236 'merge-patterns', 'merge-tools',
237 237 'hooks', 'http_proxy',
238 238 'smtp', 'patch',
239 239 'paths', 'profiling',
240 240 'server', 'trusted',
241 241 'ui', 'web', ]
242 242
243 243
244 244 def make_ui(read_from='file', path=None, checkpaths=True):
245 245 """A function that will read python rc files or database
246 246 and make an mercurial ui object from read options
247 247
248 248 :param path: path to mercurial config file
249 249 :param checkpaths: check the path
250 250 :param read_from: read from 'file' or 'db'
251 251 """
252 252
253 253 baseui = ui.ui()
254 254
255 255 #clean the baseui object
256 256 baseui._ocfg = config.config()
257 257 baseui._ucfg = config.config()
258 258 baseui._tcfg = config.config()
259 259
260 260 if read_from == 'file':
261 261 if not os.path.isfile(path):
262 262 log.warning('Unable to read config file %s' % path)
263 263 return False
264 264 log.debug('reading hgrc from %s', path)
265 265 cfg = config.config()
266 266 cfg.read(path)
267 267 for section in ui_sections:
268 268 for k, v in cfg.items(section):
269 269 log.debug('settings ui from file[%s]%s:%s', section, k, v)
270 270 baseui.setconfig(section, k, v)
271 271
272 272 elif read_from == 'db':
273 273 sa = meta.Session
274 274 ret = sa.query(RhodeCodeUi)\
275 275 .options(FromCache("sql_cache_short",
276 276 "get_hg_ui_settings")).all()
277 277
278 278 hg_ui = ret
279 279 for ui_ in hg_ui:
280 280 if ui_.ui_active:
281 281 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
282 282 ui_.ui_key, ui_.ui_value)
283 283 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
284 284
285 285 meta.Session.remove()
286 286 return baseui
287 287
288 288
289 289 def set_rhodecode_config(config):
290 290 """
291 291 Updates pylons config with new settings from database
292 292
293 293 :param config:
294 294 """
295 295 hgsettings = RhodeCodeSetting.get_app_settings()
296 296
297 297 for k, v in hgsettings.items():
298 298 config[k] = v
299 299
300 300
301 301 def invalidate_cache(cache_key, *args):
302 302 """
303 303 Puts cache invalidation task into db for
304 304 further global cache invalidation
305 305 """
306 306
307 307 from rhodecode.model.scm import ScmModel
308 308
309 309 if cache_key.startswith('get_repo_cached_'):
310 310 name = cache_key.split('get_repo_cached_')[-1]
311 311 ScmModel().mark_for_invalidation(name)
312 312
313 313
314 314 class EmptyChangeset(BaseChangeset):
315 315 """
316 316 An dummy empty changeset. It's possible to pass hash when creating
317 317 an EmptyChangeset
318 318 """
319 319
320 320 def __init__(self, cs='0' * 40, repo=None, requested_revision=None, alias=None):
321 321 self._empty_cs = cs
322 322 self.revision = -1
323 323 self.message = ''
324 324 self.author = ''
325 325 self.date = ''
326 326 self.repository = repo
327 327 self.requested_revision = requested_revision
328 328 self.alias = alias
329 329
330 330 @LazyProperty
331 331 def raw_id(self):
332 332 """
333 333 Returns raw string identifying this changeset, useful for web
334 334 representation.
335 335 """
336 336
337 337 return self._empty_cs
338 338
339 339 @LazyProperty
340 340 def branch(self):
341 341 return get_backend(self.alias).DEFAULT_BRANCH_NAME
342 342
343 343 @LazyProperty
344 344 def short_id(self):
345 345 return self.raw_id[:12]
346 346
347 347 def get_file_changeset(self, path):
348 348 return self
349 349
350 350 def get_file_content(self, path):
351 351 return u''
352 352
353 353 def get_file_size(self, path):
354 354 return 0
355 355
356 356
357 357 def map_groups(groups):
358 358 """
359 359 Checks for groups existence, and creates groups structures.
360 360 It returns last group in structure
361 361
362 362 :param groups: list of groups structure
363 363 """
364 364 sa = meta.Session
365 365
366 366 parent = None
367 367 group = None
368 368
369 369 # last element is repo in nested groups structure
370 370 groups = groups[:-1]
371 371
372 372 for lvl, group_name in enumerate(groups):
373 373 group_name = '/'.join(groups[:lvl] + [group_name])
374 374 group = sa.query(RepoGroup).filter(RepoGroup.group_name == group_name).scalar()
375 375
376 376 if group is None:
377 377 group = RepoGroup(group_name, parent)
378 378 sa.add(group)
379 379 sa.commit()
380 380 parent = group
381 381 return group
382 382
383 383
384 384 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
385 385 """
386 386 maps all repos given in initial_repo_list, non existing repositories
387 387 are created, if remove_obsolete is True it also check for db entries
388 388 that are not in initial_repo_list and removes them.
389 389
390 390 :param initial_repo_list: list of repositories found by scanning methods
391 391 :param remove_obsolete: check for obsolete entries in database
392 392 """
393 393 from rhodecode.model.repo import RepoModel
394 394 sa = meta.Session
395 395 rm = RepoModel()
396 396 user = sa.query(User).filter(User.admin == True).first()
397 397 if user is None:
398 398 raise Exception('Missing administrative account !')
399 399 added = []
400 400
401 401 for name, repo in initial_repo_list.items():
402 402 group = map_groups(name.split(Repository.url_sep()))
403 403 if not rm.get_by_repo_name(name, cache=False):
404 404 log.info('repository %s not found creating default', name)
405 405 added.append(name)
406 406 form_data = {
407 407 'repo_name': name,
408 408 'repo_name_full': name,
409 409 'repo_type': repo.alias,
410 410 'description': repo.description \
411 411 if repo.description != 'unknown' else \
412 412 '%s repository' % name,
413 413 'private': False,
414 414 'group_id': getattr(group, 'group_id', None)
415 415 }
416 416 rm.create(form_data, user, just_db=True)
417 417 sa.commit()
418 418 removed = []
419 419 if remove_obsolete:
420 420 #remove from database those repositories that are not in the filesystem
421 421 for repo in sa.query(Repository).all():
422 422 if repo.repo_name not in initial_repo_list.keys():
423 423 removed.append(repo.repo_name)
424 424 sa.delete(repo)
425 425 sa.commit()
426 426
427 427 return added, removed
428 428
429 429 # set cache regions for beaker so celery can utilise it
430 430 def add_cache(settings):
431 431 cache_settings = {'regions': None}
432 432 for key in settings.keys():
433 433 for prefix in ['beaker.cache.', 'cache.']:
434 434 if key.startswith(prefix):
435 435 name = key.split(prefix)[1].strip()
436 436 cache_settings[name] = settings[key].strip()
437 437 if cache_settings['regions']:
438 438 for region in cache_settings['regions'].split(','):
439 439 region = region.strip()
440 440 region_settings = {}
441 441 for key, value in cache_settings.items():
442 442 if key.startswith(region):
443 443 region_settings[key.split('.')[1]] = value
444 444 region_settings['expire'] = int(region_settings.get('expire',
445 445 60))
446 446 region_settings.setdefault('lock_dir',
447 447 cache_settings.get('lock_dir'))
448 448 region_settings.setdefault('data_dir',
449 449 cache_settings.get('data_dir'))
450 450
451 451 if 'type' not in region_settings:
452 452 region_settings['type'] = cache_settings.get('type',
453 453 'memory')
454 454 beaker.cache.cache_regions[region] = region_settings
455 455
456 456
457 457 #==============================================================================
458 458 # TEST FUNCTIONS AND CREATORS
459 459 #==============================================================================
460 460 def create_test_index(repo_location, config, full_index):
461 461 """
462 462 Makes default test index
463 463
464 464 :param config: test config
465 465 :param full_index:
466 466 """
467 467
468 468 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
469 469 from rhodecode.lib.pidlock import DaemonLock, LockHeld
470 470
471 471 repo_location = repo_location
472 472
473 473 index_location = os.path.join(config['app_conf']['index_dir'])
474 474 if not os.path.exists(index_location):
475 475 os.makedirs(index_location)
476 476
477 477 try:
478 478 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
479 479 WhooshIndexingDaemon(index_location=index_location,
480 480 repo_location=repo_location)\
481 481 .run(full_index=full_index)
482 482 l.release()
483 483 except LockHeld:
484 484 pass
485 485
486 486
487 487 def create_test_env(repos_test_path, config):
488 488 """
489 489 Makes a fresh database and
490 490 install test repository into tmp dir
491 491 """
492 492 from rhodecode.lib.db_manage import DbManage
493 493 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
494 494
495 495 # PART ONE create db
496 496 dbconf = config['sqlalchemy.db1.url']
497 497 log.debug('making test db %s', dbconf)
498 498
499 499 # create test dir if it doesn't exist
500 500 if not os.path.isdir(repos_test_path):
501 501 log.debug('Creating testdir %s' % repos_test_path)
502 502 os.makedirs(repos_test_path)
503 503
504 504 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
505 505 tests=True)
506 506 dbmanage.create_tables(override=True)
507 507 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
508 508 dbmanage.create_default_user()
509 509 dbmanage.admin_prompt()
510 510 dbmanage.create_permissions()
511 511 dbmanage.populate_default_permissions()
512 512 Session.commit()
513 513 # PART TWO make test repo
514 514 log.debug('making test vcs repositories')
515 515
516 516 idx_path = config['app_conf']['index_dir']
517 517 data_path = config['app_conf']['cache_dir']
518 518
519 519 #clean index and data
520 520 if idx_path and os.path.exists(idx_path):
521 521 log.debug('remove %s' % idx_path)
522 522 shutil.rmtree(idx_path)
523 523
524 524 if data_path and os.path.exists(data_path):
525 525 log.debug('remove %s' % data_path)
526 526 shutil.rmtree(data_path)
527 527
528 528 #CREATE DEFAULT HG REPOSITORY
529 529 cur_dir = dn(dn(abspath(__file__)))
530 530 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
531 531 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
532 532 tar.close()
533 533
534 534
535 535 #==============================================================================
536 536 # PASTER COMMANDS
537 537 #==============================================================================
538 538 class BasePasterCommand(Command):
539 539 """
540 540 Abstract Base Class for paster commands.
541 541
542 542 The celery commands are somewhat aggressive about loading
543 543 celery.conf, and since our module sets the `CELERY_LOADER`
544 544 environment variable to our loader, we have to bootstrap a bit and
545 545 make sure we've had a chance to load the pylons config off of the
546 546 command line, otherwise everything fails.
547 547 """
548 548 min_args = 1
549 549 min_args_error = "Please provide a paster config file as an argument."
550 550 takes_config_file = 1
551 551 requires_config_file = True
552 552
553 553 def notify_msg(self, msg, log=False):
554 554 """Make a notification to user, additionally if logger is passed
555 555 it logs this action using given logger
556 556
557 557 :param msg: message that will be printed to user
558 558 :param log: logging instance, to use to additionally log this message
559 559
560 560 """
561 561 if log and isinstance(log, logging):
562 562 log(msg)
563 563
564 564 def run(self, args):
565 565 """
566 566 Overrides Command.run
567 567
568 568 Checks for a config file argument and loads it.
569 569 """
570 570 if len(args) < self.min_args:
571 571 raise BadCommand(
572 572 self.min_args_error % {'min_args': self.min_args,
573 573 'actual_args': len(args)})
574 574
575 575 # Decrement because we're going to lob off the first argument.
576 576 # @@ This is hacky
577 577 self.min_args -= 1
578 578 self.bootstrap_config(args[0])
579 579 self.update_parser()
580 580 return super(BasePasterCommand, self).run(args[1:])
581 581
582 582 def update_parser(self):
583 583 """
584 584 Abstract method. Allows for the class's parser to be updated
585 585 before the superclass's `run` method is called. Necessary to
586 586 allow options/arguments to be passed through to the underlying
587 587 celery command.
588 588 """
589 589 raise NotImplementedError("Abstract Method.")
590 590
591 591 def bootstrap_config(self, conf):
592 592 """
593 593 Loads the pylons configuration.
594 594 """
595 595 from pylons import config as pylonsconfig
596 596
597 597 path_to_ini_file = os.path.realpath(conf)
598 598 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
599 599 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
General Comments 0
You need to be logged in to leave comments. Login now