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