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