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