##// END OF EJS Templates
avoid %r markup of unicode strings in user facing messages...
Mads Kiilerich -
r3575:ca7785fa beta
parent child Browse files
Show More
@@ -1,800 +1,800
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 re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 import decorator
36 36 import warnings
37 37 from os.path import abspath
38 38 from os.path import dirname as dn, join as jn
39 39
40 40 from paste.script.command import Command, BadCommand
41 41
42 42 from mercurial import ui, config
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45
46 46 from rhodecode.lib.vcs import get_backend
47 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model import meta
55 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
56 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.repos_group import ReposGroupModel
59 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
60 60 from rhodecode.lib.vcs.utils.fakemod import create_module
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
65 65
66 66
67 67 def recursive_replace(str_, replace=' '):
68 68 """
69 69 Recursive replace of given sign to just one instance
70 70
71 71 :param str_: given string
72 72 :param replace: char to find and replace multiple instances
73 73
74 74 Examples::
75 75 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
76 76 'Mighty-Mighty-Bo-sstones'
77 77 """
78 78
79 79 if str_.find(replace * 2) == -1:
80 80 return str_
81 81 else:
82 82 str_ = str_.replace(replace * 2, replace)
83 83 return recursive_replace(str_, replace)
84 84
85 85
86 86 def repo_name_slug(value):
87 87 """
88 88 Return slug of name of repository
89 89 This function is called on each creation/modification
90 90 of repository to prevent bad names in repo
91 91 """
92 92
93 93 slug = remove_formatting(value)
94 94 slug = strip_tags(slug)
95 95
96 96 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
97 97 slug = slug.replace(c, '-')
98 98 slug = recursive_replace(slug, '-')
99 99 slug = collapse(slug, '-')
100 100 return slug
101 101
102 102
103 103 def get_repo_slug(request):
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105 if _repo:
106 106 _repo = _repo.rstrip('/')
107 107 return _repo
108 108
109 109
110 110 def get_repos_group_slug(request):
111 111 _group = request.environ['pylons.routes_dict'].get('group_name')
112 112 if _group:
113 113 _group = _group.rstrip('/')
114 114 return _group
115 115
116 116
117 117 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
118 118 """
119 119 Action logger for various actions made by users
120 120
121 121 :param user: user that made this action, can be a unique username string or
122 122 object containing user_id attribute
123 123 :param action: action to log, should be on of predefined unique actions for
124 124 easy translations
125 125 :param repo: string name of repository or object containing repo_id,
126 126 that action was made on
127 127 :param ipaddr: optional ip address from what the action was made
128 128 :param sa: optional sqlalchemy session
129 129
130 130 """
131 131
132 132 if not sa:
133 133 sa = meta.Session()
134 134
135 135 try:
136 136 if hasattr(user, 'user_id'):
137 137 user_obj = User.get(user.user_id)
138 138 elif isinstance(user, basestring):
139 139 user_obj = User.get_by_username(user)
140 140 else:
141 141 raise Exception('You have to provide a user object or a username')
142 142
143 143 if hasattr(repo, 'repo_id'):
144 144 repo_obj = Repository.get(repo.repo_id)
145 145 repo_name = repo_obj.repo_name
146 146 elif isinstance(repo, basestring):
147 147 repo_name = repo.lstrip('/')
148 148 repo_obj = Repository.get_by_repo_name(repo_name)
149 149 else:
150 150 repo_obj = None
151 151 repo_name = ''
152 152
153 153 user_log = UserLog()
154 154 user_log.user_id = user_obj.user_id
155 155 user_log.username = user_obj.username
156 156 user_log.action = safe_unicode(action)
157 157
158 158 user_log.repository = repo_obj
159 159 user_log.repository_name = repo_name
160 160
161 161 user_log.action_date = datetime.datetime.now()
162 162 user_log.user_ip = ipaddr
163 163 sa.add(user_log)
164 164
165 165 log.info('Logging action:%s on %s by user:%s ip:%s' %
166 166 (action, safe_unicode(repo), user_obj, ipaddr))
167 167 if commit:
168 168 sa.commit()
169 169 except:
170 170 log.error(traceback.format_exc())
171 171 raise
172 172
173 173
174 174 def get_repos(path, recursive=False, skip_removed_repos=True):
175 175 """
176 176 Scans given path for repos and return (name,(type,path)) tuple
177 177
178 178 :param path: path to scan for repositories
179 179 :param recursive: recursive search and return names with subdirs in front
180 180 """
181 181
182 182 # remove ending slash for better results
183 183 path = path.rstrip(os.sep)
184 184 log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
185 185
186 186 def _get_repos(p):
187 187 if not os.access(p, os.W_OK):
188 188 return
189 189 for dirpath in os.listdir(p):
190 190 if os.path.isfile(os.path.join(p, dirpath)):
191 191 continue
192 192 cur_path = os.path.join(p, dirpath)
193 193
194 194 # skip removed repos
195 195 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
196 196 continue
197 197
198 198 #skip .<somethin> dirs
199 199 if dirpath.startswith('.'):
200 200 continue
201 201
202 202 try:
203 203 scm_info = get_scm(cur_path)
204 204 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
205 205 except VCSError:
206 206 if not recursive:
207 207 continue
208 208 #check if this dir containts other repos for recursive scan
209 209 rec_path = os.path.join(p, dirpath)
210 210 if os.path.isdir(rec_path):
211 211 for inner_scm in _get_repos(rec_path):
212 212 yield inner_scm
213 213
214 214 return _get_repos(path)
215 215
216 216 #alias for backward compat
217 217 get_filesystem_repos = get_repos
218 218
219 219
220 220 def is_valid_repo(repo_name, base_path, scm=None):
221 221 """
222 222 Returns True if given path is a valid repository False otherwise.
223 223 If scm param is given also compare if given scm is the same as expected
224 224 from scm parameter
225 225
226 226 :param repo_name:
227 227 :param base_path:
228 228 :param scm:
229 229
230 230 :return True: if given path is a valid repository
231 231 """
232 232 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
233 233
234 234 try:
235 235 scm_ = get_scm(full_path)
236 236 if scm:
237 237 return scm_[0] == scm
238 238 return True
239 239 except VCSError:
240 240 return False
241 241
242 242
243 243 def is_valid_repos_group(repos_group_name, base_path, skip_path_check=False):
244 244 """
245 245 Returns True if given path is a repos group False otherwise
246 246
247 247 :param repo_name:
248 248 :param base_path:
249 249 """
250 250 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
251 251
252 252 # check if it's not a repo
253 253 if is_valid_repo(repos_group_name, base_path):
254 254 return False
255 255
256 256 try:
257 257 # we need to check bare git repos at higher level
258 258 # since we might match branches/hooks/info/objects or possible
259 259 # other things inside bare git repo
260 260 get_scm(os.path.dirname(full_path))
261 261 return False
262 262 except VCSError:
263 263 pass
264 264
265 265 # check if it's a valid path
266 266 if skip_path_check or os.path.isdir(full_path):
267 267 return True
268 268
269 269 return False
270 270
271 271
272 272 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
273 273 while True:
274 274 ok = raw_input(prompt)
275 275 if ok in ('y', 'ye', 'yes'):
276 276 return True
277 277 if ok in ('n', 'no', 'nop', 'nope'):
278 278 return False
279 279 retries = retries - 1
280 280 if retries < 0:
281 281 raise IOError
282 282 print complaint
283 283
284 284 #propagated from mercurial documentation
285 285 ui_sections = ['alias', 'auth',
286 286 'decode/encode', 'defaults',
287 287 'diff', 'email',
288 288 'extensions', 'format',
289 289 'merge-patterns', 'merge-tools',
290 290 'hooks', 'http_proxy',
291 291 'smtp', 'patch',
292 292 'paths', 'profiling',
293 293 'server', 'trusted',
294 294 'ui', 'web', ]
295 295
296 296
297 297 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
298 298 """
299 299 A function that will read python rc files or database
300 300 and make an mercurial ui object from read options
301 301
302 302 :param path: path to mercurial config file
303 303 :param checkpaths: check the path
304 304 :param read_from: read from 'file' or 'db'
305 305 """
306 306
307 307 baseui = ui.ui()
308 308
309 309 # clean the baseui object
310 310 baseui._ocfg = config.config()
311 311 baseui._ucfg = config.config()
312 312 baseui._tcfg = config.config()
313 313
314 314 if read_from == 'file':
315 315 if not os.path.isfile(path):
316 316 log.debug('hgrc file is not present at %s, skipping...' % path)
317 317 return False
318 318 log.debug('reading hgrc from %s' % path)
319 319 cfg = config.config()
320 320 cfg.read(path)
321 321 for section in ui_sections:
322 322 for k, v in cfg.items(section):
323 323 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
324 324 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
325 325
326 326 elif read_from == 'db':
327 327 sa = meta.Session()
328 328 ret = sa.query(RhodeCodeUi)\
329 329 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
330 330 .all()
331 331
332 332 hg_ui = ret
333 333 for ui_ in hg_ui:
334 334 if ui_.ui_active:
335 335 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
336 336 ui_.ui_key, ui_.ui_value)
337 337 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
338 338 safe_str(ui_.ui_value))
339 339 if ui_.ui_key == 'push_ssl':
340 340 # force set push_ssl requirement to False, rhodecode
341 341 # handles that
342 342 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
343 343 False)
344 344 if clear_session:
345 345 meta.Session.remove()
346 346 return baseui
347 347
348 348
349 349 def set_rhodecode_config(config):
350 350 """
351 351 Updates pylons config with new settings from database
352 352
353 353 :param config:
354 354 """
355 355 hgsettings = RhodeCodeSetting.get_app_settings()
356 356
357 357 for k, v in hgsettings.items():
358 358 config[k] = v
359 359
360 360
361 361 def invalidate_cache(cache_key, *args):
362 362 """
363 363 Puts cache invalidation task into db for
364 364 further global cache invalidation
365 365 """
366 366
367 367 from rhodecode.model.scm import ScmModel
368 368
369 369 if cache_key.startswith('get_repo_cached_'):
370 370 name = cache_key.split('get_repo_cached_')[-1]
371 371 ScmModel().mark_for_invalidation(name)
372 372
373 373
374 374 def map_groups(path):
375 375 """
376 376 Given a full path to a repository, create all nested groups that this
377 377 repo is inside. This function creates parent-child relationships between
378 378 groups and creates default perms for all new groups.
379 379
380 380 :param paths: full path to repository
381 381 """
382 382 sa = meta.Session()
383 383 groups = path.split(Repository.url_sep())
384 384 parent = None
385 385 group = None
386 386
387 387 # last element is repo in nested groups structure
388 388 groups = groups[:-1]
389 389 rgm = ReposGroupModel(sa)
390 390 for lvl, group_name in enumerate(groups):
391 391 group_name = '/'.join(groups[:lvl] + [group_name])
392 392 group = RepoGroup.get_by_group_name(group_name)
393 393 desc = '%s group' % group_name
394 394
395 395 # skip folders that are now removed repos
396 396 if REMOVED_REPO_PAT.match(group_name):
397 397 break
398 398
399 399 if group is None:
400 400 log.debug('creating group level: %s group_name: %s' % (lvl,
401 401 group_name))
402 402 group = RepoGroup(group_name, parent)
403 403 group.group_description = desc
404 404 sa.add(group)
405 405 rgm._create_default_perms(group)
406 406 sa.flush()
407 407 parent = group
408 408 return group
409 409
410 410
411 411 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
412 412 install_git_hook=False):
413 413 """
414 414 maps all repos given in initial_repo_list, non existing repositories
415 415 are created, if remove_obsolete is True it also check for db entries
416 416 that are not in initial_repo_list and removes them.
417 417
418 418 :param initial_repo_list: list of repositories found by scanning methods
419 419 :param remove_obsolete: check for obsolete entries in database
420 420 :param install_git_hook: if this is True, also check and install githook
421 421 for a repo if missing
422 422 """
423 423 from rhodecode.model.repo import RepoModel
424 424 from rhodecode.model.scm import ScmModel
425 425 sa = meta.Session()
426 426 rm = RepoModel()
427 427 user = sa.query(User).filter(User.admin == True).first()
428 428 if user is None:
429 429 raise Exception('Missing administrative account!')
430 430 added = []
431 431
432 432 # # clear cache keys
433 433 # log.debug("Clearing cache keys now...")
434 434 # CacheInvalidation.clear_cache()
435 435 # sa.commit()
436 436
437 437 ##creation defaults
438 438 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
439 439 enable_statistics = defs.get('repo_enable_statistics')
440 440 enable_locking = defs.get('repo_enable_locking')
441 441 enable_downloads = defs.get('repo_enable_downloads')
442 442 private = defs.get('repo_private')
443 443
444 444 for name, repo in initial_repo_list.items():
445 445 group = map_groups(name)
446 446 db_repo = rm.get_by_repo_name(name)
447 447 # found repo that is on filesystem not in RhodeCode database
448 448 if not db_repo:
449 449 log.info('repository %s not found, creating now' % name)
450 450 added.append(name)
451 451 desc = (repo.description
452 452 if repo.description != 'unknown'
453 453 else '%s repository' % name)
454 454
455 455 new_repo = rm.create_repo(
456 456 repo_name=name,
457 457 repo_type=repo.alias,
458 458 description=desc,
459 459 repos_group=getattr(group, 'group_id', None),
460 460 owner=user,
461 461 just_db=True,
462 462 enable_locking=enable_locking,
463 463 enable_downloads=enable_downloads,
464 464 enable_statistics=enable_statistics,
465 465 private=private
466 466 )
467 467 # we added that repo just now, and make sure it has githook
468 468 # installed
469 469 if new_repo.repo_type == 'git':
470 470 ScmModel().install_git_hook(new_repo.scm_instance)
471 471 new_repo.update_changeset_cache()
472 472 elif install_git_hook:
473 473 if db_repo.repo_type == 'git':
474 474 ScmModel().install_git_hook(db_repo.scm_instance)
475 475 # during starting install all cache keys for all repositories in the
476 476 # system, this will register all repos and multiple instances
477 477 key, _prefix, _org_key = CacheInvalidation._get_key(name)
478 478 CacheInvalidation.invalidate(name)
479 479 log.debug("Creating a cache key for %s, instance_id %s"
480 480 % (name, _prefix or 'unknown'))
481 481
482 482 sa.commit()
483 483 removed = []
484 484 if remove_obsolete:
485 485 # remove from database those repositories that are not in the filesystem
486 486 for repo in sa.query(Repository).all():
487 487 if repo.repo_name not in initial_repo_list.keys():
488 488 log.debug("Removing non-existing repository found in db `%s`" %
489 489 repo.repo_name)
490 490 try:
491 491 sa.delete(repo)
492 492 sa.commit()
493 493 removed.append(repo.repo_name)
494 494 except:
495 495 #don't hold further removals on error
496 496 log.error(traceback.format_exc())
497 497 sa.rollback()
498 498 return added, removed
499 499
500 500
501 501 # set cache regions for beaker so celery can utilise it
502 502 def add_cache(settings):
503 503 cache_settings = {'regions': None}
504 504 for key in settings.keys():
505 505 for prefix in ['beaker.cache.', 'cache.']:
506 506 if key.startswith(prefix):
507 507 name = key.split(prefix)[1].strip()
508 508 cache_settings[name] = settings[key].strip()
509 509 if cache_settings['regions']:
510 510 for region in cache_settings['regions'].split(','):
511 511 region = region.strip()
512 512 region_settings = {}
513 513 for key, value in cache_settings.items():
514 514 if key.startswith(region):
515 515 region_settings[key.split('.')[1]] = value
516 516 region_settings['expire'] = int(region_settings.get('expire',
517 517 60))
518 518 region_settings.setdefault('lock_dir',
519 519 cache_settings.get('lock_dir'))
520 520 region_settings.setdefault('data_dir',
521 521 cache_settings.get('data_dir'))
522 522
523 523 if 'type' not in region_settings:
524 524 region_settings['type'] = cache_settings.get('type',
525 525 'memory')
526 526 beaker.cache.cache_regions[region] = region_settings
527 527
528 528
529 529 def load_rcextensions(root_path):
530 530 import rhodecode
531 531 from rhodecode.config import conf
532 532
533 533 path = os.path.join(root_path, 'rcextensions', '__init__.py')
534 534 if os.path.isfile(path):
535 535 rcext = create_module('rc', path)
536 536 EXT = rhodecode.EXTENSIONS = rcext
537 537 log.debug('Found rcextensions now loading %s...' % rcext)
538 538
539 539 # Additional mappings that are not present in the pygments lexers
540 540 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
541 541
542 542 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
543 543
544 544 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
545 545 log.debug('settings custom INDEX_EXTENSIONS')
546 546 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
547 547
548 548 #ADDITIONAL MAPPINGS
549 549 log.debug('adding extra into INDEX_EXTENSIONS')
550 550 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
551 551
552 552 # auto check if the module is not missing any data, set to default if is
553 553 # this will help autoupdate new feature of rcext module
554 554 from rhodecode.config import rcextensions
555 555 for k in dir(rcextensions):
556 556 if not k.startswith('_') and not hasattr(EXT, k):
557 557 setattr(EXT, k, getattr(rcextensions, k))
558 558
559 559
560 560 def get_custom_lexer(extension):
561 561 """
562 562 returns a custom lexer if it's defined in rcextensions module, or None
563 563 if there's no custom lexer defined
564 564 """
565 565 import rhodecode
566 566 from pygments import lexers
567 567 #check if we didn't define this extension as other lexer
568 568 if rhodecode.EXTENSIONS and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
569 569 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
570 570 return lexers.get_lexer_by_name(_lexer_name)
571 571
572 572
573 573 #==============================================================================
574 574 # TEST FUNCTIONS AND CREATORS
575 575 #==============================================================================
576 576 def create_test_index(repo_location, config, full_index):
577 577 """
578 578 Makes default test index
579 579
580 580 :param config: test config
581 581 :param full_index:
582 582 """
583 583
584 584 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
585 585 from rhodecode.lib.pidlock import DaemonLock, LockHeld
586 586
587 587 repo_location = repo_location
588 588
589 589 index_location = os.path.join(config['app_conf']['index_dir'])
590 590 if not os.path.exists(index_location):
591 591 os.makedirs(index_location)
592 592
593 593 try:
594 594 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
595 595 WhooshIndexingDaemon(index_location=index_location,
596 596 repo_location=repo_location)\
597 597 .run(full_index=full_index)
598 598 l.release()
599 599 except LockHeld:
600 600 pass
601 601
602 602
603 603 def create_test_env(repos_test_path, config):
604 604 """
605 605 Makes a fresh database and
606 606 install test repository into tmp dir
607 607 """
608 608 from rhodecode.lib.db_manage import DbManage
609 609 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
610 610
611 611 # PART ONE create db
612 612 dbconf = config['sqlalchemy.db1.url']
613 613 log.debug('making test db %s' % dbconf)
614 614
615 615 # create test dir if it doesn't exist
616 616 if not os.path.isdir(repos_test_path):
617 617 log.debug('Creating testdir %s' % repos_test_path)
618 618 os.makedirs(repos_test_path)
619 619
620 620 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
621 621 tests=True)
622 622 dbmanage.create_tables(override=True)
623 623 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
624 624 dbmanage.create_default_user()
625 625 dbmanage.admin_prompt()
626 626 dbmanage.create_permissions()
627 627 dbmanage.populate_default_permissions()
628 628 Session().commit()
629 629 # PART TWO make test repo
630 630 log.debug('making test vcs repositories')
631 631
632 632 idx_path = config['app_conf']['index_dir']
633 633 data_path = config['app_conf']['cache_dir']
634 634
635 635 #clean index and data
636 636 if idx_path and os.path.exists(idx_path):
637 637 log.debug('remove %s' % idx_path)
638 638 shutil.rmtree(idx_path)
639 639
640 640 if data_path and os.path.exists(data_path):
641 641 log.debug('remove %s' % data_path)
642 642 shutil.rmtree(data_path)
643 643
644 644 #CREATE DEFAULT TEST REPOS
645 645 cur_dir = dn(dn(abspath(__file__)))
646 646 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
647 647 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
648 648 tar.close()
649 649
650 650 cur_dir = dn(dn(abspath(__file__)))
651 651 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
652 652 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
653 653 tar.close()
654 654
655 655 #LOAD VCS test stuff
656 656 from rhodecode.tests.vcs import setup_package
657 657 setup_package()
658 658
659 659
660 660 #==============================================================================
661 661 # PASTER COMMANDS
662 662 #==============================================================================
663 663 class BasePasterCommand(Command):
664 664 """
665 665 Abstract Base Class for paster commands.
666 666
667 667 The celery commands are somewhat aggressive about loading
668 668 celery.conf, and since our module sets the `CELERY_LOADER`
669 669 environment variable to our loader, we have to bootstrap a bit and
670 670 make sure we've had a chance to load the pylons config off of the
671 671 command line, otherwise everything fails.
672 672 """
673 673 min_args = 1
674 674 min_args_error = "Please provide a paster config file as an argument."
675 675 takes_config_file = 1
676 676 requires_config_file = True
677 677
678 678 def notify_msg(self, msg, log=False):
679 679 """Make a notification to user, additionally if logger is passed
680 680 it logs this action using given logger
681 681
682 682 :param msg: message that will be printed to user
683 683 :param log: logging instance, to use to additionally log this message
684 684
685 685 """
686 686 if log and isinstance(log, logging):
687 687 log(msg)
688 688
689 689 def run(self, args):
690 690 """
691 691 Overrides Command.run
692 692
693 693 Checks for a config file argument and loads it.
694 694 """
695 695 if len(args) < self.min_args:
696 696 raise BadCommand(
697 697 self.min_args_error % {'min_args': self.min_args,
698 698 'actual_args': len(args)})
699 699
700 700 # Decrement because we're going to lob off the first argument.
701 701 # @@ This is hacky
702 702 self.min_args -= 1
703 703 self.bootstrap_config(args[0])
704 704 self.update_parser()
705 705 return super(BasePasterCommand, self).run(args[1:])
706 706
707 707 def update_parser(self):
708 708 """
709 709 Abstract method. Allows for the class's parser to be updated
710 710 before the superclass's `run` method is called. Necessary to
711 711 allow options/arguments to be passed through to the underlying
712 712 celery command.
713 713 """
714 714 raise NotImplementedError("Abstract Method.")
715 715
716 716 def bootstrap_config(self, conf):
717 717 """
718 718 Loads the pylons configuration.
719 719 """
720 720 from pylons import config as pylonsconfig
721 721
722 722 self.path_to_ini_file = os.path.realpath(conf)
723 723 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
724 724 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
725 725
726 726 def _init_session(self):
727 727 """
728 728 Inits SqlAlchemy Session
729 729 """
730 730 logging.config.fileConfig(self.path_to_ini_file)
731 731 from pylons import config
732 732 from rhodecode.model import init_model
733 733 from rhodecode.lib.utils2 import engine_from_config
734 734
735 735 #get to remove repos !!
736 736 add_cache(config)
737 737 engine = engine_from_config(config, 'sqlalchemy.db1.')
738 738 init_model(engine)
739 739
740 740
741 741 def check_git_version():
742 742 """
743 743 Checks what version of git is installed in system, and issues a warning
744 744 if it's too old for RhodeCode to properly work.
745 745 """
746 746 from rhodecode import BACKENDS
747 747 from rhodecode.lib.vcs.backends.git.repository import GitRepository
748 748 from distutils.version import StrictVersion
749 749
750 750 stdout, stderr = GitRepository._run_git_command('--version', _bare=True,
751 751 _safe=True)
752 752
753 753 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
754 754 if len(ver.split('.')) > 3:
755 755 #StrictVersion needs to be only 3 element type
756 756 ver = '.'.join(ver.split('.')[:3])
757 757 try:
758 758 _ver = StrictVersion(ver)
759 759 except:
760 760 _ver = StrictVersion('0.0.0')
761 761 stderr = traceback.format_exc()
762 762
763 763 req_ver = '1.7.4'
764 764 to_old_git = False
765 765 if _ver < StrictVersion(req_ver):
766 766 to_old_git = True
767 767
768 768 if 'git' in BACKENDS:
769 769 log.debug('GIT version detected: %s' % stdout)
770 770 if stderr:
771 log.warning('Unable to detect git version org error was:%r' % stderr)
771 log.warning('Unable to detect git version, org error was: %r' % stderr)
772 772 elif to_old_git:
773 773 log.warning('RhodeCode detected git version %s, which is too old '
774 774 'for the system to function properly. Make sure '
775 775 'its version is at least %s' % (ver, req_ver))
776 776 return _ver
777 777
778 778
779 779 @decorator.decorator
780 780 def jsonify(func, *args, **kwargs):
781 781 """Action decorator that formats output for JSON
782 782
783 783 Given a function that will return content, this decorator will turn
784 784 the result into JSON, with a content-type of 'application/json' and
785 785 output it.
786 786
787 787 """
788 788 from pylons.decorators.util import get_pylons
789 789 from rhodecode.lib.ext_json import json
790 790 pylons = get_pylons(args)
791 791 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
792 792 data = func(*args, **kwargs)
793 793 if isinstance(data, (list, tuple)):
794 794 msg = "JSON responses with Array envelopes are susceptible to " \
795 795 "cross-site data leak attacks, see " \
796 796 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
797 797 warnings.warn(msg, Warning, 2)
798 798 log.warning(msg)
799 799 log.debug("Returning JSON wrapped action output")
800 800 return json.dumps(data, encoding='utf-8')
@@ -1,551 +1,551
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5 import rhodecode
6 6 from rhodecode.lib.vcs.conf import settings
7 7 from rhodecode.lib.vcs.exceptions import RepositoryError
8 8 from rhodecode.lib.vcs.exceptions import ChangesetError
9 9 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
10 10 from rhodecode.lib.vcs.exceptions import VCSError
11 11 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
12 12 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
13 13 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
14 14 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
15 15 RemovedFileNode, SubModuleNode, ChangedFileNodesGenerator,\
16 16 AddedFileNodesGenerator, RemovedFileNodesGenerator
17 17 from rhodecode.lib.vcs.utils import safe_unicode
18 18 from rhodecode.lib.vcs.utils import date_fromtimestamp
19 19 from rhodecode.lib.vcs.utils.lazy import LazyProperty
20 20 from rhodecode.lib.utils2 import safe_int
21 21
22 22
23 23 class GitChangeset(BaseChangeset):
24 24 """
25 25 Represents state of the repository at single revision.
26 26 """
27 27
28 28 def __init__(self, repository, revision):
29 29 self._stat_modes = {}
30 30 self.repository = repository
31 31
32 32 try:
33 33 commit = self.repository._repo.get_object(revision)
34 34 if isinstance(commit, objects.Tag):
35 35 revision = commit.object[1]
36 36 commit = self.repository._repo.get_object(commit.object[1])
37 37 except KeyError:
38 38 raise RepositoryError("Cannot get object with id %s" % revision)
39 39 self.raw_id = revision
40 40 self.id = self.raw_id
41 41 self.short_id = self.raw_id[:12]
42 42 self._commit = commit
43 43
44 44 self._tree_id = commit.tree
45 45 self._committer_property = 'committer'
46 46 self._author_property = 'author'
47 47 self._date_property = 'commit_time'
48 48 self._date_tz_property = 'commit_timezone'
49 49 self.revision = repository.revisions.index(revision)
50 50
51 51 self.message = safe_unicode(commit.message)
52 52
53 53 self.nodes = {}
54 54 self._paths = {}
55 55
56 56 @LazyProperty
57 57 def committer(self):
58 58 return safe_unicode(getattr(self._commit, self._committer_property))
59 59
60 60 @LazyProperty
61 61 def author(self):
62 62 return safe_unicode(getattr(self._commit, self._author_property))
63 63
64 64 @LazyProperty
65 65 def date(self):
66 66 return date_fromtimestamp(getattr(self._commit, self._date_property),
67 67 getattr(self._commit, self._date_tz_property))
68 68
69 69 @LazyProperty
70 70 def _timestamp(self):
71 71 return getattr(self._commit, self._date_property)
72 72
73 73 @LazyProperty
74 74 def status(self):
75 75 """
76 76 Returns modified, added, removed, deleted files for current changeset
77 77 """
78 78 return self.changed, self.added, self.removed
79 79
80 80 @LazyProperty
81 81 def tags(self):
82 82 _tags = []
83 83 for tname, tsha in self.repository.tags.iteritems():
84 84 if tsha == self.raw_id:
85 85 _tags.append(tname)
86 86 return _tags
87 87
88 88 @LazyProperty
89 89 def branch(self):
90 90
91 91 heads = self.repository._heads(reverse=False)
92 92
93 93 ref = heads.get(self.raw_id)
94 94 if ref:
95 95 return safe_unicode(ref)
96 96
97 97 def _fix_path(self, path):
98 98 """
99 99 Paths are stored without trailing slash so we need to get rid off it if
100 100 needed.
101 101 """
102 102 if path.endswith('/'):
103 103 path = path.rstrip('/')
104 104 return path
105 105
106 106 def _get_id_for_path(self, path):
107 107
108 108 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
109 109 if not path in self._paths:
110 110 path = path.strip('/')
111 111 # set root tree
112 112 tree = self.repository._repo[self._tree_id]
113 113 if path == '':
114 114 self._paths[''] = tree.id
115 115 return tree.id
116 116 splitted = path.split('/')
117 117 dirs, name = splitted[:-1], splitted[-1]
118 118 curdir = ''
119 119
120 120 # initially extract things from root dir
121 121 for item, stat, id in tree.iteritems():
122 122 if curdir:
123 123 name = '/'.join((curdir, item))
124 124 else:
125 125 name = item
126 126 self._paths[name] = id
127 127 self._stat_modes[name] = stat
128 128
129 129 for dir in dirs:
130 130 if curdir:
131 131 curdir = '/'.join((curdir, dir))
132 132 else:
133 133 curdir = dir
134 134 dir_id = None
135 135 for item, stat, id in tree.iteritems():
136 136 if dir == item:
137 137 dir_id = id
138 138 if dir_id:
139 139 # Update tree
140 140 tree = self.repository._repo[dir_id]
141 141 if not isinstance(tree, objects.Tree):
142 142 raise ChangesetError('%s is not a directory' % curdir)
143 143 else:
144 144 raise ChangesetError('%s have not been found' % curdir)
145 145
146 146 # cache all items from the given traversed tree
147 147 for item, stat, id in tree.iteritems():
148 148 if curdir:
149 149 name = '/'.join((curdir, item))
150 150 else:
151 151 name = item
152 152 self._paths[name] = id
153 153 self._stat_modes[name] = stat
154 154 if not path in self._paths:
155 155 raise NodeDoesNotExistError("There is no file nor directory "
156 "at the given path %r at revision %r"
156 "at the given path '%s' at revision %s"
157 157 % (path, self.short_id))
158 158 return self._paths[path]
159 159
160 160 def _get_kind(self, path):
161 161 obj = self.repository._repo[self._get_id_for_path(path)]
162 162 if isinstance(obj, objects.Blob):
163 163 return NodeKind.FILE
164 164 elif isinstance(obj, objects.Tree):
165 165 return NodeKind.DIR
166 166
167 167 def _get_filectx(self, path):
168 168 path = self._fix_path(path)
169 169 if self._get_kind(path) != NodeKind.FILE:
170 raise ChangesetError("File does not exist for revision %r at "
171 " %r" % (self.raw_id, path))
170 raise ChangesetError("File does not exist for revision %s at "
171 " '%s'" % (self.raw_id, path))
172 172 return path
173 173
174 174 def _get_file_nodes(self):
175 175 return chain(*(t[2] for t in self.walk()))
176 176
177 177 @LazyProperty
178 178 def parents(self):
179 179 """
180 180 Returns list of parents changesets.
181 181 """
182 182 return [self.repository.get_changeset(parent)
183 183 for parent in self._commit.parents]
184 184
185 185 @LazyProperty
186 186 def children(self):
187 187 """
188 188 Returns list of children changesets.
189 189 """
190 190 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
191 191 '--all').strip()
192 192 so, se = self.repository.run_git_command(
193 193 "rev-list %s --children | grep '^%s'" % (rev_filter, self.raw_id)
194 194 )
195 195
196 196 children = []
197 197 for l in so.splitlines():
198 198 childs = l.split(' ')[1:]
199 199 children.extend(childs)
200 200 return [self.repository.get_changeset(cs) for cs in children]
201 201
202 202 def next(self, branch=None):
203 203
204 204 if branch and self.branch != branch:
205 205 raise VCSError('Branch option used on changeset not belonging '
206 206 'to that branch')
207 207
208 208 def _next(changeset, branch):
209 209 try:
210 210 next_ = changeset.revision + 1
211 211 next_rev = changeset.repository.revisions[next_]
212 212 except IndexError:
213 213 raise ChangesetDoesNotExistError
214 214 cs = changeset.repository.get_changeset(next_rev)
215 215
216 216 if branch and branch != cs.branch:
217 217 return _next(cs, branch)
218 218
219 219 return cs
220 220
221 221 return _next(self, branch)
222 222
223 223 def prev(self, branch=None):
224 224 if branch and self.branch != branch:
225 225 raise VCSError('Branch option used on changeset not belonging '
226 226 'to that branch')
227 227
228 228 def _prev(changeset, branch):
229 229 try:
230 230 prev_ = changeset.revision - 1
231 231 if prev_ < 0:
232 232 raise IndexError
233 233 prev_rev = changeset.repository.revisions[prev_]
234 234 except IndexError:
235 235 raise ChangesetDoesNotExistError
236 236
237 237 cs = changeset.repository.get_changeset(prev_rev)
238 238
239 239 if branch and branch != cs.branch:
240 240 return _prev(cs, branch)
241 241
242 242 return cs
243 243
244 244 return _prev(self, branch)
245 245
246 246 def diff(self, ignore_whitespace=True, context=3):
247 247 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
248 248 rev2 = self
249 249 return ''.join(self.repository.get_diff(rev1, rev2,
250 250 ignore_whitespace=ignore_whitespace,
251 251 context=context))
252 252
253 253 def get_file_mode(self, path):
254 254 """
255 255 Returns stat mode of the file at the given ``path``.
256 256 """
257 257 # ensure path is traversed
258 258 self._get_id_for_path(path)
259 259 return self._stat_modes[path]
260 260
261 261 def get_file_content(self, path):
262 262 """
263 263 Returns content of the file at given ``path``.
264 264 """
265 265 id = self._get_id_for_path(path)
266 266 blob = self.repository._repo[id]
267 267 return blob.as_pretty_string()
268 268
269 269 def get_file_size(self, path):
270 270 """
271 271 Returns size of the file at given ``path``.
272 272 """
273 273 id = self._get_id_for_path(path)
274 274 blob = self.repository._repo[id]
275 275 return blob.raw_length()
276 276
277 277 def get_file_changeset(self, path):
278 278 """
279 279 Returns last commit of the file at the given ``path``.
280 280 """
281 281 return self.get_file_history(path, limit=1)[0]
282 282
283 283 def get_file_history(self, path, limit=None):
284 284 """
285 285 Returns history of file as reversed list of ``Changeset`` objects for
286 286 which file at given ``path`` has been modified.
287 287
288 288 TODO: This function now uses os underlying 'git' and 'grep' commands
289 289 which is generally not good. Should be replaced with algorithm
290 290 iterating commits.
291 291 """
292 292
293 293 self._get_filectx(path)
294 294 if limit:
295 295 cmd = 'log -n %s --pretty="format: %%H" -s -p %s -- "%s"' % (
296 296 safe_int(limit, 0), self.id, path
297 297 )
298 298 else:
299 299 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
300 300 self.id, path
301 301 )
302 302 so, se = self.repository.run_git_command(cmd)
303 303 ids = re.findall(r'[0-9a-fA-F]{40}', so)
304 304 return [self.repository.get_changeset(id) for id in ids]
305 305
306 306 def get_file_history_2(self, path):
307 307 """
308 308 Returns history of file as reversed list of ``Changeset`` objects for
309 309 which file at given ``path`` has been modified.
310 310
311 311 """
312 312 self._get_filectx(path)
313 313 from dulwich.walk import Walker
314 314 include = [self.id]
315 315 walker = Walker(self.repository._repo.object_store, include,
316 316 paths=[path], max_entries=1)
317 317 return [self.repository.get_changeset(sha)
318 318 for sha in (x.commit.id for x in walker)]
319 319
320 320 def get_file_annotate(self, path):
321 321 """
322 322 Returns a generator of four element tuples with
323 323 lineno, sha, changeset lazy loader and line
324 324
325 325 TODO: This function now uses os underlying 'git' command which is
326 326 generally not good. Should be replaced with algorithm iterating
327 327 commits.
328 328 """
329 329 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
330 330 # -l ==> outputs long shas (and we need all 40 characters)
331 331 # --root ==> doesn't put '^' character for bounderies
332 332 # -r sha ==> blames for the given revision
333 333 so, se = self.repository.run_git_command(cmd)
334 334
335 335 for i, blame_line in enumerate(so.split('\n')[:-1]):
336 336 ln_no = i + 1
337 337 sha, line = re.split(r' ', blame_line, 1)
338 338 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
339 339
340 340 def fill_archive(self, stream=None, kind='tgz', prefix=None,
341 341 subrepos=False):
342 342 """
343 343 Fills up given stream.
344 344
345 345 :param stream: file like object.
346 346 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
347 347 Default: ``tgz``.
348 348 :param prefix: name of root directory in archive.
349 349 Default is repository name and changeset's raw_id joined with dash
350 350 (``repo-tip.<KIND>``).
351 351 :param subrepos: include subrepos in this archive.
352 352
353 353 :raise ImproperArchiveTypeError: If given kind is wrong.
354 354 :raise VcsError: If given stream is None
355 355
356 356 """
357 357 allowed_kinds = settings.ARCHIVE_SPECS.keys()
358 358 if kind not in allowed_kinds:
359 359 raise ImproperArchiveTypeError('Archive kind not supported use one'
360 360 'of %s', allowed_kinds)
361 361
362 362 if prefix is None:
363 363 prefix = '%s-%s' % (self.repository.name, self.short_id)
364 364 elif prefix.startswith('/'):
365 365 raise VCSError("Prefix cannot start with leading slash")
366 366 elif prefix.strip() == '':
367 367 raise VCSError("Prefix cannot be empty")
368 368
369 369 if kind == 'zip':
370 370 frmt = 'zip'
371 371 else:
372 372 frmt = 'tar'
373 373 _git_path = rhodecode.CONFIG.get('git_path', 'git')
374 374 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
375 375 frmt, prefix, self.raw_id)
376 376 if kind == 'tgz':
377 377 cmd += ' | gzip -9'
378 378 elif kind == 'tbz2':
379 379 cmd += ' | bzip2 -9'
380 380
381 381 if stream is None:
382 382 raise VCSError('You need to pass in a valid stream for filling'
383 383 ' with archival data')
384 384 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
385 385 cwd=self.repository.path)
386 386
387 387 buffer_size = 1024 * 8
388 388 chunk = popen.stdout.read(buffer_size)
389 389 while chunk:
390 390 stream.write(chunk)
391 391 chunk = popen.stdout.read(buffer_size)
392 392 # Make sure all descriptors would be read
393 393 popen.communicate()
394 394
395 395 def get_nodes(self, path):
396 396 if self._get_kind(path) != NodeKind.DIR:
397 raise ChangesetError("Directory does not exist for revision %r at "
398 " %r" % (self.revision, path))
397 raise ChangesetError("Directory does not exist for revision %s at "
398 " '%s'" % (self.revision, path))
399 399 path = self._fix_path(path)
400 400 id = self._get_id_for_path(path)
401 401 tree = self.repository._repo[id]
402 402 dirnodes = []
403 403 filenodes = []
404 404 als = self.repository.alias
405 405 for name, stat, id in tree.iteritems():
406 406 if objects.S_ISGITLINK(stat):
407 407 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
408 408 alias=als))
409 409 continue
410 410
411 411 obj = self.repository._repo.get_object(id)
412 412 if path != '':
413 413 obj_path = '/'.join((path, name))
414 414 else:
415 415 obj_path = name
416 416 if obj_path not in self._stat_modes:
417 417 self._stat_modes[obj_path] = stat
418 418 if isinstance(obj, objects.Tree):
419 419 dirnodes.append(DirNode(obj_path, changeset=self))
420 420 elif isinstance(obj, objects.Blob):
421 421 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
422 422 else:
423 423 raise ChangesetError("Requested object should be Tree "
424 424 "or Blob, is %r" % type(obj))
425 425 nodes = dirnodes + filenodes
426 426 for node in nodes:
427 427 if not node.path in self.nodes:
428 428 self.nodes[node.path] = node
429 429 nodes.sort()
430 430 return nodes
431 431
432 432 def get_node(self, path):
433 433 if isinstance(path, unicode):
434 434 path = path.encode('utf-8')
435 435 path = self._fix_path(path)
436 436 if not path in self.nodes:
437 437 try:
438 438 id_ = self._get_id_for_path(path)
439 439 except ChangesetError:
440 440 raise NodeDoesNotExistError("Cannot find one of parents' "
441 441 "directories for a given path: %s" % path)
442 442
443 443 _GL = lambda m: m and objects.S_ISGITLINK(m)
444 444 if _GL(self._stat_modes.get(path)):
445 445 node = SubModuleNode(path, url=None, changeset=id_,
446 446 alias=self.repository.alias)
447 447 else:
448 448 obj = self.repository._repo.get_object(id_)
449 449
450 450 if isinstance(obj, objects.Tree):
451 451 if path == '':
452 452 node = RootNode(changeset=self)
453 453 else:
454 454 node = DirNode(path, changeset=self)
455 455 node._tree = obj
456 456 elif isinstance(obj, objects.Blob):
457 457 node = FileNode(path, changeset=self)
458 458 node._blob = obj
459 459 else:
460 460 raise NodeDoesNotExistError("There is no file nor directory "
461 "at the given path %r at revision %r"
461 "at the given path '%s' at revision %s"
462 462 % (path, self.short_id))
463 463 # cache node
464 464 self.nodes[path] = node
465 465 return self.nodes[path]
466 466
467 467 @LazyProperty
468 468 def affected_files(self):
469 469 """
470 470 Get's a fast accessible file changes for given changeset
471 471 """
472 472 a, m, d = self._changes_cache
473 473 return list(a.union(m).union(d))
474 474
475 475 @LazyProperty
476 476 def _diff_name_status(self):
477 477 output = []
478 478 for parent in self.parents:
479 479 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
480 480 self.raw_id)
481 481 so, se = self.repository.run_git_command(cmd)
482 482 output.append(so.strip())
483 483 return '\n'.join(output)
484 484
485 485 @LazyProperty
486 486 def _changes_cache(self):
487 487 added = set()
488 488 modified = set()
489 489 deleted = set()
490 490 _r = self.repository._repo
491 491
492 492 parents = self.parents
493 493 if not self.parents:
494 494 parents = [EmptyChangeset()]
495 495 for parent in parents:
496 496 if isinstance(parent, EmptyChangeset):
497 497 oid = None
498 498 else:
499 499 oid = _r[parent.raw_id].tree
500 500 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
501 501 for (oldpath, newpath), (_, _), (_, _) in changes:
502 502 if newpath and oldpath:
503 503 modified.add(newpath)
504 504 elif newpath and not oldpath:
505 505 added.add(newpath)
506 506 elif not newpath and oldpath:
507 507 deleted.add(oldpath)
508 508 return added, modified, deleted
509 509
510 510 def _get_paths_for_status(self, status):
511 511 """
512 512 Returns sorted list of paths for given ``status``.
513 513
514 514 :param status: one of: *added*, *modified* or *deleted*
515 515 """
516 516 a, m, d = self._changes_cache
517 517 return sorted({
518 518 'added': list(a),
519 519 'modified': list(m),
520 520 'deleted': list(d)}[status]
521 521 )
522 522
523 523 @LazyProperty
524 524 def added(self):
525 525 """
526 526 Returns list of added ``FileNode`` objects.
527 527 """
528 528 if not self.parents:
529 529 return list(self._get_file_nodes())
530 530 return AddedFileNodesGenerator([n for n in
531 531 self._get_paths_for_status('added')], self)
532 532
533 533 @LazyProperty
534 534 def changed(self):
535 535 """
536 536 Returns list of modified ``FileNode`` objects.
537 537 """
538 538 if not self.parents:
539 539 return []
540 540 return ChangedFileNodesGenerator([n for n in
541 541 self._get_paths_for_status('modified')], self)
542 542
543 543 @LazyProperty
544 544 def removed(self):
545 545 """
546 546 Returns list of removed ``FileNode`` objects.
547 547 """
548 548 if not self.parents:
549 549 return []
550 550 return RemovedFileNodesGenerator([n for n in
551 551 self._get_paths_for_status('deleted')], self)
@@ -1,698 +1,698
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import re
14 14 import time
15 15 import posixpath
16 16 import logging
17 17 import traceback
18 18 import urllib
19 19 import urllib2
20 20 from dulwich.repo import Repo, NotGitRepository
21 21 from dulwich.objects import Tag
22 22 from string import Template
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 35 from rhodecode.lib.vcs.utils.paths import abspath
36 36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 37 from .workdir import GitWorkdir
38 38 from .changeset import GitChangeset
39 39 from .inmemory import GitInMemoryChangeset
40 40 from .config import ConfigFile
41 41 from rhodecode.lib import subprocessio
42 42
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class GitRepository(BaseRepository):
48 48 """
49 49 Git repository backend.
50 50 """
51 51 DEFAULT_BRANCH_NAME = 'master'
52 52 scm = 'git'
53 53
54 54 def __init__(self, repo_path, create=False, src_url=None,
55 55 update_after_clone=False, bare=False):
56 56
57 57 self.path = abspath(repo_path)
58 58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 59 self.bare = repo.bare
60 60
61 61 self._config_files = [
62 62 bare and abspath(self.path, 'config')
63 63 or abspath(self.path, '.git', 'config'),
64 64 abspath(get_user_home(), '.gitconfig'),
65 65 ]
66 66
67 67 @ThreadLocalLazyProperty
68 68 def _repo(self):
69 69 repo = Repo(self.path)
70 70 # patch the instance of GitRepo with an "FAKE" ui object to add
71 71 # compatibility layer with Mercurial
72 72 if not hasattr(repo, 'ui'):
73 73 from mercurial.ui import ui
74 74 baseui = ui()
75 75 setattr(repo, 'ui', baseui)
76 76 return repo
77 77
78 78 @property
79 79 def head(self):
80 80 try:
81 81 return self._repo.head()
82 82 except KeyError:
83 83 return None
84 84
85 85 @LazyProperty
86 86 def revisions(self):
87 87 """
88 88 Returns list of revisions' ids, in ascending order. Being lazy
89 89 attribute allows external tools to inject shas from cache.
90 90 """
91 91 return self._get_all_revisions()
92 92
93 93 @classmethod
94 94 def _run_git_command(cls, cmd, **opts):
95 95 """
96 96 Runs given ``cmd`` as git command and returns tuple
97 97 (stdout, stderr).
98 98
99 99 :param cmd: git command to be executed
100 100 :param opts: env options to pass into Subprocess command
101 101 """
102 102
103 103 if '_bare' in opts:
104 104 _copts = []
105 105 del opts['_bare']
106 106 else:
107 107 _copts = ['-c', 'core.quotepath=false', ]
108 108 safe_call = False
109 109 if '_safe' in opts:
110 110 #no exc on failure
111 111 del opts['_safe']
112 112 safe_call = True
113 113
114 114 _str_cmd = False
115 115 if isinstance(cmd, basestring):
116 116 cmd = [cmd]
117 117 _str_cmd = True
118 118
119 119 gitenv = os.environ
120 120 # need to clean fix GIT_DIR !
121 121 if 'GIT_DIR' in gitenv:
122 122 del gitenv['GIT_DIR']
123 123 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
124 124
125 125 _git_path = rhodecode.CONFIG.get('git_path', 'git')
126 126 cmd = [_git_path] + _copts + cmd
127 127 if _str_cmd:
128 128 cmd = ' '.join(cmd)
129 129 try:
130 130 _opts = dict(
131 131 env=gitenv,
132 132 shell=False,
133 133 )
134 134 _opts.update(opts)
135 135 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
136 136 except (EnvironmentError, OSError), err:
137 137 tb_err = ("Couldn't run git command (%s).\n"
138 138 "Original error was:%s\n" % (cmd, err))
139 139 log.error(tb_err)
140 140 if safe_call:
141 141 return '', err
142 142 else:
143 143 raise RepositoryError(tb_err)
144 144
145 145 return ''.join(p.output), ''.join(p.error)
146 146
147 147 def run_git_command(self, cmd):
148 148 opts = {}
149 149 if os.path.isdir(self.path):
150 150 opts['cwd'] = self.path
151 151 return self._run_git_command(cmd, **opts)
152 152
153 153 @classmethod
154 154 def _check_url(cls, url):
155 155 """
156 156 Functon will check given url and try to verify if it's a valid
157 157 link. Sometimes it may happened that mercurial will issue basic
158 158 auth request that can cause whole API to hang when used from python
159 159 or other external calls.
160 160
161 161 On failures it'll raise urllib2.HTTPError
162 162 """
163 163 from mercurial.util import url as Url
164 164
165 165 # those authnadlers are patched for python 2.6.5 bug an
166 166 # infinit looping when given invalid resources
167 167 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
168 168
169 169 # check first if it's not an local url
170 170 if os.path.isdir(url) or url.startswith('file:'):
171 171 return True
172 172
173 173 if('+' in url[:url.find('://')]):
174 174 url = url[url.find('+') + 1:]
175 175
176 176 handlers = []
177 177 test_uri, authinfo = Url(url).authinfo()
178 178 if not test_uri.endswith('info/refs'):
179 179 test_uri = test_uri.rstrip('/') + '/info/refs'
180 180 if authinfo:
181 181 #create a password manager
182 182 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
183 183 passmgr.add_password(*authinfo)
184 184
185 185 handlers.extend((httpbasicauthhandler(passmgr),
186 186 httpdigestauthhandler(passmgr)))
187 187
188 188 o = urllib2.build_opener(*handlers)
189 189 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
190 190
191 191 q = {"service": 'git-upload-pack'}
192 192 qs = '?%s' % urllib.urlencode(q)
193 193 cu = "%s%s" % (test_uri, qs)
194 194 req = urllib2.Request(cu, None, {})
195 195
196 196 try:
197 197 resp = o.open(req)
198 198 return resp.code == 200
199 199 except Exception, e:
200 200 # means it cannot be cloned
201 201 raise urllib2.URLError("[%s] %s" % (url, e))
202 202
203 203 def _get_repo(self, create, src_url=None, update_after_clone=False,
204 204 bare=False):
205 205 if create and os.path.exists(self.path):
206 206 raise RepositoryError("Location already exist")
207 207 if src_url and not create:
208 208 raise RepositoryError("Create should be set to True if src_url is "
209 209 "given (clone operation creates repository)")
210 210 try:
211 211 if create and src_url:
212 212 GitRepository._check_url(src_url)
213 213 self.clone(src_url, update_after_clone, bare)
214 214 return Repo(self.path)
215 215 elif create:
216 216 os.mkdir(self.path)
217 217 if bare:
218 218 return Repo.init_bare(self.path)
219 219 else:
220 220 return Repo.init(self.path)
221 221 else:
222 222 return self._repo
223 223 except (NotGitRepository, OSError), err:
224 224 raise RepositoryError(err)
225 225
226 226 def _get_all_revisions(self):
227 227 # we must check if this repo is not empty, since later command
228 228 # fails if it is. And it's cheaper to ask than throw the subprocess
229 229 # errors
230 230 try:
231 231 self._repo.head()
232 232 except KeyError:
233 233 return []
234 234 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
235 235 '--all').strip()
236 236 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
237 237 try:
238 238 so, se = self.run_git_command(cmd)
239 239 except RepositoryError:
240 240 # Can be raised for empty repositories
241 241 return []
242 242 return so.splitlines()
243 243
244 244 def _get_all_revisions2(self):
245 245 #alternate implementation using dulwich
246 246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
247 247 if x[1][1] != 'T']
248 248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
249 249
250 250 def _get_revision(self, revision):
251 251 """
252 252 For git backend we always return integer here. This way we ensure
253 253 that changset's revision attribute would become integer.
254 254 """
255 255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
256 256 is_bstr = lambda o: isinstance(o, (str, unicode))
257 257 is_null = lambda o: len(o) == revision.count('0')
258 258
259 259 if len(self.revisions) == 0:
260 260 raise EmptyRepositoryError("There are no changesets yet")
261 261
262 262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
263 263 revision = self.revisions[-1]
264 264
265 265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
266 266 or isinstance(revision, int) or is_null(revision)):
267 267 try:
268 268 revision = self.revisions[int(revision)]
269 269 except:
270 raise ChangesetDoesNotExistError("Revision %r does not exist "
270 raise ChangesetDoesNotExistError("Revision %s does not exist "
271 271 "for this repository" % (revision))
272 272
273 273 elif is_bstr(revision):
274 274 # get by branch/tag name
275 275 _ref_revision = self._parsed_refs.get(revision)
276 276 _tags_shas = self.tags.values()
277 277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
278 278 return _ref_revision[0]
279 279
280 280 # maybe it's a tag ? we don't have them in self.revisions
281 281 elif revision in _tags_shas:
282 282 return _tags_shas[_tags_shas.index(revision)]
283 283
284 284 elif not pattern.match(revision) or revision not in self.revisions:
285 raise ChangesetDoesNotExistError("Revision %r does not exist "
285 raise ChangesetDoesNotExistError("Revision %s does not exist "
286 286 "for this repository" % (revision))
287 287
288 288 # Ensure we return full id
289 289 if not pattern.match(str(revision)):
290 raise ChangesetDoesNotExistError("Given revision %r not recognized"
290 raise ChangesetDoesNotExistError("Given revision %s not recognized"
291 291 % revision)
292 292 return revision
293 293
294 294 def _get_archives(self, archive_name='tip'):
295 295
296 296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
297 297 yield {"type": i[0], "extension": i[1], "node": archive_name}
298 298
299 299 def _get_url(self, url):
300 300 """
301 301 Returns normalized url. If schema is not given, would fall to
302 302 filesystem (``file:///``) schema.
303 303 """
304 304 url = str(url)
305 305 if url != 'default' and not '://' in url:
306 306 url = ':///'.join(('file', url))
307 307 return url
308 308
309 309 def get_hook_location(self):
310 310 """
311 311 returns absolute path to location where hooks are stored
312 312 """
313 313 loc = os.path.join(self.path, 'hooks')
314 314 if not self.bare:
315 315 loc = os.path.join(self.path, '.git', 'hooks')
316 316 return loc
317 317
318 318 @LazyProperty
319 319 def name(self):
320 320 return os.path.basename(self.path)
321 321
322 322 @LazyProperty
323 323 def last_change(self):
324 324 """
325 325 Returns last change made on this repository as datetime object
326 326 """
327 327 return date_fromtimestamp(self._get_mtime(), makedate()[1])
328 328
329 329 def _get_mtime(self):
330 330 try:
331 331 return time.mktime(self.get_changeset().date.timetuple())
332 332 except RepositoryError:
333 333 idx_loc = '' if self.bare else '.git'
334 334 # fallback to filesystem
335 335 in_path = os.path.join(self.path, idx_loc, "index")
336 336 he_path = os.path.join(self.path, idx_loc, "HEAD")
337 337 if os.path.exists(in_path):
338 338 return os.stat(in_path).st_mtime
339 339 else:
340 340 return os.stat(he_path).st_mtime
341 341
342 342 @LazyProperty
343 343 def description(self):
344 344 idx_loc = '' if self.bare else '.git'
345 345 undefined_description = u'unknown'
346 346 description_path = os.path.join(self.path, idx_loc, 'description')
347 347 if os.path.isfile(description_path):
348 348 return safe_unicode(open(description_path).read())
349 349 else:
350 350 return undefined_description
351 351
352 352 @LazyProperty
353 353 def contact(self):
354 354 undefined_contact = u'Unknown'
355 355 return undefined_contact
356 356
357 357 @property
358 358 def branches(self):
359 359 if not self.revisions:
360 360 return {}
361 361 sortkey = lambda ctx: ctx[0]
362 362 _branches = [(x[0], x[1][0])
363 363 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
364 364 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
365 365
366 366 @LazyProperty
367 367 def tags(self):
368 368 return self._get_tags()
369 369
370 370 def _get_tags(self):
371 371 if not self.revisions:
372 372 return {}
373 373
374 374 sortkey = lambda ctx: ctx[0]
375 375 _tags = [(x[0], x[1][0])
376 376 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
377 377 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
378 378
379 379 def tag(self, name, user, revision=None, message=None, date=None,
380 380 **kwargs):
381 381 """
382 382 Creates and returns a tag for the given ``revision``.
383 383
384 384 :param name: name for new tag
385 385 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
386 386 :param revision: changeset id for which new tag would be created
387 387 :param message: message of the tag's commit
388 388 :param date: date of tag's commit
389 389
390 390 :raises TagAlreadyExistError: if tag with same name already exists
391 391 """
392 392 if name in self.tags:
393 393 raise TagAlreadyExistError("Tag %s already exists" % name)
394 394 changeset = self.get_changeset(revision)
395 395 message = message or "Added tag %s for commit %s" % (name,
396 396 changeset.raw_id)
397 397 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
398 398
399 399 self._parsed_refs = self._get_parsed_refs()
400 400 self.tags = self._get_tags()
401 401 return changeset
402 402
403 403 def remove_tag(self, name, user, message=None, date=None):
404 404 """
405 405 Removes tag with the given ``name``.
406 406
407 407 :param name: name of the tag to be removed
408 408 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
409 409 :param message: message of the tag's removal commit
410 410 :param date: date of tag's removal commit
411 411
412 412 :raises TagDoesNotExistError: if tag with given name does not exists
413 413 """
414 414 if name not in self.tags:
415 415 raise TagDoesNotExistError("Tag %s does not exist" % name)
416 416 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
417 417 try:
418 418 os.remove(tagpath)
419 419 self._parsed_refs = self._get_parsed_refs()
420 420 self.tags = self._get_tags()
421 421 except OSError, e:
422 422 raise RepositoryError(e.strerror)
423 423
424 424 @LazyProperty
425 425 def _parsed_refs(self):
426 426 return self._get_parsed_refs()
427 427
428 428 def _get_parsed_refs(self):
429 429 refs = self._repo.get_refs()
430 430 keys = [('refs/heads/', 'H'),
431 431 ('refs/remotes/origin/', 'RH'),
432 432 ('refs/tags/', 'T')]
433 433 _refs = {}
434 434 for ref, sha in refs.iteritems():
435 435 for k, type_ in keys:
436 436 if ref.startswith(k):
437 437 _key = ref[len(k):]
438 438 if type_ == 'T':
439 439 obj = self._repo.get_object(sha)
440 440 if isinstance(obj, Tag):
441 441 sha = self._repo.get_object(sha).object[1]
442 442 _refs[_key] = [sha, type_]
443 443 break
444 444 return _refs
445 445
446 446 def _heads(self, reverse=False):
447 447 refs = self._repo.get_refs()
448 448 heads = {}
449 449
450 450 for key, val in refs.items():
451 451 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
452 452 if key.startswith(ref_key):
453 453 n = key[len(ref_key):]
454 454 if n not in ['HEAD']:
455 455 heads[n] = val
456 456
457 457 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
458 458
459 459 def get_changeset(self, revision=None):
460 460 """
461 461 Returns ``GitChangeset`` object representing commit from git repository
462 462 at the given revision or head (most recent commit) if None given.
463 463 """
464 464 if isinstance(revision, GitChangeset):
465 465 return revision
466 466 revision = self._get_revision(revision)
467 467 changeset = GitChangeset(repository=self, revision=revision)
468 468 return changeset
469 469
470 470 def get_changesets(self, start=None, end=None, start_date=None,
471 471 end_date=None, branch_name=None, reverse=False):
472 472 """
473 473 Returns iterator of ``GitChangeset`` objects from start to end (both
474 474 are inclusive), in ascending date order (unless ``reverse`` is set).
475 475
476 476 :param start: changeset ID, as str; first returned changeset
477 477 :param end: changeset ID, as str; last returned changeset
478 478 :param start_date: if specified, changesets with commit date less than
479 479 ``start_date`` would be filtered out from returned set
480 480 :param end_date: if specified, changesets with commit date greater than
481 481 ``end_date`` would be filtered out from returned set
482 482 :param branch_name: if specified, changesets not reachable from given
483 483 branch would be filtered out from returned set
484 484 :param reverse: if ``True``, returned generator would be reversed
485 485 (meaning that returned changesets would have descending date order)
486 486
487 487 :raise BranchDoesNotExistError: If given ``branch_name`` does not
488 488 exist.
489 489 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
490 490 ``end`` could not be found.
491 491
492 492 """
493 493 if branch_name and branch_name not in self.branches:
494 494 raise BranchDoesNotExistError("Branch '%s' not found" \
495 495 % branch_name)
496 496 # %H at format means (full) commit hash, initial hashes are retrieved
497 497 # in ascending date order
498 498 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
499 499 cmd_params = {}
500 500 if start_date:
501 501 cmd_template += ' --since "$since"'
502 502 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
503 503 if end_date:
504 504 cmd_template += ' --until "$until"'
505 505 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
506 506 if branch_name:
507 507 cmd_template += ' $branch_name'
508 508 cmd_params['branch_name'] = branch_name
509 509 else:
510 510 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
511 511 '--all').strip()
512 512 cmd_template += ' %s' % (rev_filter)
513 513
514 514 cmd = Template(cmd_template).safe_substitute(**cmd_params)
515 515 revs = self.run_git_command(cmd)[0].splitlines()
516 516 start_pos = 0
517 517 end_pos = len(revs)
518 518 if start:
519 519 _start = self._get_revision(start)
520 520 try:
521 521 start_pos = revs.index(_start)
522 522 except ValueError:
523 523 pass
524 524
525 525 if end is not None:
526 526 _end = self._get_revision(end)
527 527 try:
528 528 end_pos = revs.index(_end)
529 529 except ValueError:
530 530 pass
531 531
532 532 if None not in [start, end] and start_pos > end_pos:
533 533 raise RepositoryError('start cannot be after end')
534 534
535 535 if end_pos is not None:
536 536 end_pos += 1
537 537
538 538 revs = revs[start_pos:end_pos]
539 539 if reverse:
540 540 revs = reversed(revs)
541 541 for rev in revs:
542 542 yield self.get_changeset(rev)
543 543
544 544 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
545 545 context=3):
546 546 """
547 547 Returns (git like) *diff*, as plain text. Shows changes introduced by
548 548 ``rev2`` since ``rev1``.
549 549
550 550 :param rev1: Entry point from which diff is shown. Can be
551 551 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
552 552 the changes since empty state of the repository until ``rev2``
553 553 :param rev2: Until which revision changes should be shown.
554 554 :param ignore_whitespace: If set to ``True``, would not show whitespace
555 555 changes. Defaults to ``False``.
556 556 :param context: How many lines before/after changed lines should be
557 557 shown. Defaults to ``3``.
558 558 """
559 559 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
560 560 if ignore_whitespace:
561 561 flags.append('-w')
562 562
563 563 if hasattr(rev1, 'raw_id'):
564 564 rev1 = getattr(rev1, 'raw_id')
565 565
566 566 if hasattr(rev2, 'raw_id'):
567 567 rev2 = getattr(rev2, 'raw_id')
568 568
569 569 if rev1 == self.EMPTY_CHANGESET:
570 570 rev2 = self.get_changeset(rev2).raw_id
571 571 cmd = ' '.join(['show'] + flags + [rev2])
572 572 else:
573 573 rev1 = self.get_changeset(rev1).raw_id
574 574 rev2 = self.get_changeset(rev2).raw_id
575 575 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
576 576
577 577 if path:
578 578 cmd += ' -- "%s"' % path
579 579
580 580 stdout, stderr = self.run_git_command(cmd)
581 581 # If we used 'show' command, strip first few lines (until actual diff
582 582 # starts)
583 583 if rev1 == self.EMPTY_CHANGESET:
584 584 lines = stdout.splitlines()
585 585 x = 0
586 586 for line in lines:
587 587 if line.startswith('diff'):
588 588 break
589 589 x += 1
590 590 # Append new line just like 'diff' command do
591 591 stdout = '\n'.join(lines[x:]) + '\n'
592 592 return stdout
593 593
594 594 @LazyProperty
595 595 def in_memory_changeset(self):
596 596 """
597 597 Returns ``GitInMemoryChangeset`` object for this repository.
598 598 """
599 599 return GitInMemoryChangeset(self)
600 600
601 601 def clone(self, url, update_after_clone=True, bare=False):
602 602 """
603 603 Tries to clone changes from external location.
604 604
605 605 :param update_after_clone: If set to ``False``, git won't checkout
606 606 working directory
607 607 :param bare: If set to ``True``, repository would be cloned into
608 608 *bare* git repository (no working directory at all).
609 609 """
610 610 url = self._get_url(url)
611 611 cmd = ['clone']
612 612 if bare:
613 613 cmd.append('--bare')
614 614 elif not update_after_clone:
615 615 cmd.append('--no-checkout')
616 616 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
617 617 cmd = ' '.join(cmd)
618 618 # If error occurs run_git_command raises RepositoryError already
619 619 self.run_git_command(cmd)
620 620
621 621 def pull(self, url):
622 622 """
623 623 Tries to pull changes from external location.
624 624 """
625 625 url = self._get_url(url)
626 626 cmd = ['pull']
627 627 cmd.append("--ff-only")
628 628 cmd.append(url)
629 629 cmd = ' '.join(cmd)
630 630 # If error occurs run_git_command raises RepositoryError already
631 631 self.run_git_command(cmd)
632 632
633 633 def fetch(self, url):
634 634 """
635 635 Tries to pull changes from external location.
636 636 """
637 637 url = self._get_url(url)
638 638 so, se = self.run_git_command('ls-remote -h %s' % url)
639 639 refs = []
640 640 for line in (x for x in so.splitlines()):
641 641 sha, ref = line.split('\t')
642 642 refs.append(ref)
643 643 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
644 644 cmd = '''fetch %s -- %s''' % (url, refs)
645 645 self.run_git_command(cmd)
646 646
647 647 @LazyProperty
648 648 def workdir(self):
649 649 """
650 650 Returns ``Workdir`` instance for this repository.
651 651 """
652 652 return GitWorkdir(self)
653 653
654 654 def get_config_value(self, section, name, config_file=None):
655 655 """
656 656 Returns configuration value for a given [``section``] and ``name``.
657 657
658 658 :param section: Section we want to retrieve value from
659 659 :param name: Name of configuration we want to retrieve
660 660 :param config_file: A path to file which should be used to retrieve
661 661 configuration from (might also be a list of file paths)
662 662 """
663 663 if config_file is None:
664 664 config_file = []
665 665 elif isinstance(config_file, basestring):
666 666 config_file = [config_file]
667 667
668 668 def gen_configs():
669 669 for path in config_file + self._config_files:
670 670 try:
671 671 yield ConfigFile.from_path(path)
672 672 except (IOError, OSError, ValueError):
673 673 continue
674 674
675 675 for config in gen_configs():
676 676 try:
677 677 return config.get(section, name)
678 678 except KeyError:
679 679 continue
680 680 return None
681 681
682 682 def get_user_name(self, config_file=None):
683 683 """
684 684 Returns user's name from global configuration file.
685 685
686 686 :param config_file: A path to file which should be used to retrieve
687 687 configuration from (might also be a list of file paths)
688 688 """
689 689 return self.get_config_value('user', 'name', config_file)
690 690
691 691 def get_user_email(self, config_file=None):
692 692 """
693 693 Returns user's email from global configuration file.
694 694
695 695 :param config_file: A path to file which should be used to retrieve
696 696 configuration from (might also be a list of file paths)
697 697 """
698 698 return self.get_config_value('user', 'email', config_file)
@@ -1,379 +1,379
1 1 import os
2 2 import posixpath
3 3
4 4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
9 9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
10 10 RemovedFileNodesGenerator, RootNode, SubModuleNode
11 11
12 12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
13 13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
14 14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
15 15 from rhodecode.lib.vcs.utils.hgcompat import archival, hex
16 16
17 17
18 18 class MercurialChangeset(BaseChangeset):
19 19 """
20 20 Represents state of the repository at the single revision.
21 21 """
22 22
23 23 def __init__(self, repository, revision):
24 24 self.repository = repository
25 25 self.raw_id = revision
26 26 self._ctx = repository._repo[revision]
27 27 self.revision = self._ctx._rev
28 28 self.nodes = {}
29 29
30 30 @LazyProperty
31 31 def tags(self):
32 32 return map(safe_unicode, self._ctx.tags())
33 33
34 34 @LazyProperty
35 35 def branch(self):
36 36 return safe_unicode(self._ctx.branch())
37 37
38 38 @LazyProperty
39 39 def bookmarks(self):
40 40 return map(safe_unicode, self._ctx.bookmarks())
41 41
42 42 @LazyProperty
43 43 def message(self):
44 44 return safe_unicode(self._ctx.description())
45 45
46 46 @LazyProperty
47 47 def committer(self):
48 48 return safe_unicode(self.author)
49 49
50 50 @LazyProperty
51 51 def author(self):
52 52 return safe_unicode(self._ctx.user())
53 53
54 54 @LazyProperty
55 55 def date(self):
56 56 return date_fromtimestamp(*self._ctx.date())
57 57
58 58 @LazyProperty
59 59 def _timestamp(self):
60 60 return self._ctx.date()[0]
61 61
62 62 @LazyProperty
63 63 def status(self):
64 64 """
65 65 Returns modified, added, removed, deleted files for current changeset
66 66 """
67 67 return self.repository._repo.status(self._ctx.p1().node(),
68 68 self._ctx.node())
69 69
70 70 @LazyProperty
71 71 def _file_paths(self):
72 72 return list(self._ctx)
73 73
74 74 @LazyProperty
75 75 def _dir_paths(self):
76 76 p = list(set(get_dirs_for_path(*self._file_paths)))
77 77 p.insert(0, '')
78 78 return p
79 79
80 80 @LazyProperty
81 81 def _paths(self):
82 82 return self._dir_paths + self._file_paths
83 83
84 84 @LazyProperty
85 85 def id(self):
86 86 if self.last:
87 87 return u'tip'
88 88 return self.short_id
89 89
90 90 @LazyProperty
91 91 def short_id(self):
92 92 return self.raw_id[:12]
93 93
94 94 @LazyProperty
95 95 def parents(self):
96 96 """
97 97 Returns list of parents changesets.
98 98 """
99 99 return [self.repository.get_changeset(parent.rev())
100 100 for parent in self._ctx.parents() if parent.rev() >= 0]
101 101
102 102 @LazyProperty
103 103 def children(self):
104 104 """
105 105 Returns list of children changesets.
106 106 """
107 107 return [self.repository.get_changeset(child.rev())
108 108 for child in self._ctx.children() if child.rev() >= 0]
109 109
110 110 def next(self, branch=None):
111 111
112 112 if branch and self.branch != branch:
113 113 raise VCSError('Branch option used on changeset not belonging '
114 114 'to that branch')
115 115
116 116 def _next(changeset, branch):
117 117 try:
118 118 next_ = changeset.revision + 1
119 119 next_rev = changeset.repository.revisions[next_]
120 120 except IndexError:
121 121 raise ChangesetDoesNotExistError
122 122 cs = changeset.repository.get_changeset(next_rev)
123 123
124 124 if branch and branch != cs.branch:
125 125 return _next(cs, branch)
126 126
127 127 return cs
128 128
129 129 return _next(self, branch)
130 130
131 131 def prev(self, branch=None):
132 132 if branch and self.branch != branch:
133 133 raise VCSError('Branch option used on changeset not belonging '
134 134 'to that branch')
135 135
136 136 def _prev(changeset, branch):
137 137 try:
138 138 prev_ = changeset.revision - 1
139 139 if prev_ < 0:
140 140 raise IndexError
141 141 prev_rev = changeset.repository.revisions[prev_]
142 142 except IndexError:
143 143 raise ChangesetDoesNotExistError
144 144
145 145 cs = changeset.repository.get_changeset(prev_rev)
146 146
147 147 if branch and branch != cs.branch:
148 148 return _prev(cs, branch)
149 149
150 150 return cs
151 151
152 152 return _prev(self, branch)
153 153
154 154 def diff(self, ignore_whitespace=True, context=3):
155 155 return ''.join(self._ctx.diff(git=True,
156 156 ignore_whitespace=ignore_whitespace,
157 157 context=context))
158 158
159 159 def _fix_path(self, path):
160 160 """
161 161 Paths are stored without trailing slash so we need to get rid off it if
162 162 needed. Also mercurial keeps filenodes as str so we need to decode
163 163 from unicode to str
164 164 """
165 165 if path.endswith('/'):
166 166 path = path.rstrip('/')
167 167
168 168 return safe_str(path)
169 169
170 170 def _get_kind(self, path):
171 171 path = self._fix_path(path)
172 172 if path in self._file_paths:
173 173 return NodeKind.FILE
174 174 elif path in self._dir_paths:
175 175 return NodeKind.DIR
176 176 else:
177 raise ChangesetError("Node does not exist at the given path %r"
177 raise ChangesetError("Node does not exist at the given path '%s'"
178 178 % (path))
179 179
180 180 def _get_filectx(self, path):
181 181 path = self._fix_path(path)
182 182 if self._get_kind(path) != NodeKind.FILE:
183 raise ChangesetError("File does not exist for revision %r at "
184 " %r" % (self.raw_id, path))
183 raise ChangesetError("File does not exist for revision %s at "
184 " '%s'" % (self.raw_id, path))
185 185 return self._ctx.filectx(path)
186 186
187 187 def _extract_submodules(self):
188 188 """
189 189 returns a dictionary with submodule information from substate file
190 190 of hg repository
191 191 """
192 192 return self._ctx.substate
193 193
194 194 def get_file_mode(self, path):
195 195 """
196 196 Returns stat mode of the file at the given ``path``.
197 197 """
198 198 fctx = self._get_filectx(path)
199 199 if 'x' in fctx.flags():
200 200 return 0100755
201 201 else:
202 202 return 0100644
203 203
204 204 def get_file_content(self, path):
205 205 """
206 206 Returns content of the file at given ``path``.
207 207 """
208 208 fctx = self._get_filectx(path)
209 209 return fctx.data()
210 210
211 211 def get_file_size(self, path):
212 212 """
213 213 Returns size of the file at given ``path``.
214 214 """
215 215 fctx = self._get_filectx(path)
216 216 return fctx.size()
217 217
218 218 def get_file_changeset(self, path):
219 219 """
220 220 Returns last commit of the file at the given ``path``.
221 221 """
222 222 return self.get_file_history(path, limit=1)[0]
223 223
224 224 def get_file_history(self, path, limit=None):
225 225 """
226 226 Returns history of file as reversed list of ``Changeset`` objects for
227 227 which file at given ``path`` has been modified.
228 228 """
229 229 fctx = self._get_filectx(path)
230 230 hist = []
231 231 cnt = 0
232 232 for cs in reversed([x for x in fctx.filelog()]):
233 233 cnt += 1
234 234 hist.append(hex(fctx.filectx(cs).node()))
235 235 if limit and cnt == limit:
236 236 break
237 237
238 238 return [self.repository.get_changeset(node) for node in hist]
239 239
240 240 def get_file_annotate(self, path):
241 241 """
242 242 Returns a generator of four element tuples with
243 243 lineno, sha, changeset lazy loader and line
244 244 """
245 245
246 246 fctx = self._get_filectx(path)
247 247 for i, annotate_data in enumerate(fctx.annotate()):
248 248 ln_no = i + 1
249 249 sha = hex(annotate_data[0].node())
250 250 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), annotate_data[1],)
251 251
252 252 def fill_archive(self, stream=None, kind='tgz', prefix=None,
253 253 subrepos=False):
254 254 """
255 255 Fills up given stream.
256 256
257 257 :param stream: file like object.
258 258 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
259 259 Default: ``tgz``.
260 260 :param prefix: name of root directory in archive.
261 261 Default is repository name and changeset's raw_id joined with dash
262 262 (``repo-tip.<KIND>``).
263 263 :param subrepos: include subrepos in this archive.
264 264
265 265 :raise ImproperArchiveTypeError: If given kind is wrong.
266 266 :raise VcsError: If given stream is None
267 267 """
268 268
269 269 allowed_kinds = settings.ARCHIVE_SPECS.keys()
270 270 if kind not in allowed_kinds:
271 271 raise ImproperArchiveTypeError('Archive kind not supported use one'
272 272 'of %s', allowed_kinds)
273 273
274 274 if stream is None:
275 275 raise VCSError('You need to pass in a valid stream for filling'
276 276 ' with archival data')
277 277
278 278 if prefix is None:
279 279 prefix = '%s-%s' % (self.repository.name, self.short_id)
280 280 elif prefix.startswith('/'):
281 281 raise VCSError("Prefix cannot start with leading slash")
282 282 elif prefix.strip() == '':
283 283 raise VCSError("Prefix cannot be empty")
284 284
285 285 archival.archive(self.repository._repo, stream, self.raw_id,
286 286 kind, prefix=prefix, subrepos=subrepos)
287 287
288 288 if stream.closed and hasattr(stream, 'name'):
289 289 stream = open(stream.name, 'rb')
290 290 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
291 291 stream = open(stream.name, 'rb')
292 292 else:
293 293 stream.seek(0)
294 294
295 295 def get_nodes(self, path):
296 296 """
297 297 Returns combined ``DirNode`` and ``FileNode`` objects list representing
298 298 state of changeset at the given ``path``. If node at the given ``path``
299 299 is not instance of ``DirNode``, ChangesetError would be raised.
300 300 """
301 301
302 302 if self._get_kind(path) != NodeKind.DIR:
303 raise ChangesetError("Directory does not exist for revision %r at "
304 " %r" % (self.revision, path))
303 raise ChangesetError("Directory does not exist for revision %s at "
304 " '%s'" % (self.revision, path))
305 305 path = self._fix_path(path)
306 306
307 307 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
308 308 if os.path.dirname(f) == path]
309 309 dirs = path == '' and '' or [d for d in self._dir_paths
310 310 if d and posixpath.dirname(d) == path]
311 311 dirnodes = [DirNode(d, changeset=self) for d in dirs
312 312 if os.path.dirname(d) == path]
313 313
314 314 als = self.repository.alias
315 315 for k, vals in self._extract_submodules().iteritems():
316 316 #vals = url,rev,type
317 317 loc = vals[0]
318 318 cs = vals[1]
319 319 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
320 320 alias=als))
321 321 nodes = dirnodes + filenodes
322 322 # cache nodes
323 323 for node in nodes:
324 324 self.nodes[node.path] = node
325 325 nodes.sort()
326 326
327 327 return nodes
328 328
329 329 def get_node(self, path):
330 330 """
331 331 Returns ``Node`` object from the given ``path``. If there is no node at
332 332 the given ``path``, ``ChangesetError`` would be raised.
333 333 """
334 334
335 335 path = self._fix_path(path)
336 336
337 337 if not path in self.nodes:
338 338 if path in self._file_paths:
339 339 node = FileNode(path, changeset=self)
340 340 elif path in self._dir_paths or path in self._dir_paths:
341 341 if path == '':
342 342 node = RootNode(changeset=self)
343 343 else:
344 344 node = DirNode(path, changeset=self)
345 345 else:
346 346 raise NodeDoesNotExistError("There is no file nor directory "
347 "at the given path: %r at revision %r"
347 "at the given path: '%s' at revision %s"
348 348 % (path, self.short_id))
349 349 # cache node
350 350 self.nodes[path] = node
351 351 return self.nodes[path]
352 352
353 353 @LazyProperty
354 354 def affected_files(self):
355 355 """
356 356 Get's a fast accessible file changes for given changeset
357 357 """
358 358 return self._ctx.files()
359 359
360 360 @property
361 361 def added(self):
362 362 """
363 363 Returns list of added ``FileNode`` objects.
364 364 """
365 365 return AddedFileNodesGenerator([n for n in self.status[1]], self)
366 366
367 367 @property
368 368 def changed(self):
369 369 """
370 370 Returns list of modified ``FileNode`` objects.
371 371 """
372 372 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
373 373
374 374 @property
375 375 def removed(self):
376 376 """
377 377 Returns list of removed ``FileNode`` objects.
378 378 """
379 379 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,549 +1,549
1 1 import os
2 2 import time
3 3 import datetime
4 4 import urllib
5 5 import urllib2
6 6
7 7 from rhodecode.lib.vcs.backends.base import BaseRepository
8 8 from .workdir import MercurialWorkdir
9 9 from .changeset import MercurialChangeset
10 10 from .inmemory import MercurialInMemoryChangeset
11 11
12 12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 16 makedate, safe_unicode
17 17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 19 from rhodecode.lib.vcs.utils.paths import abspath
20 20
21 21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
22 22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
23 23
24 24
25 25 class MercurialRepository(BaseRepository):
26 26 """
27 27 Mercurial repository backend
28 28 """
29 29 DEFAULT_BRANCH_NAME = 'default'
30 30 scm = 'hg'
31 31
32 32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 33 update_after_clone=False):
34 34 """
35 35 Raises RepositoryError if repository could not be find at the given
36 36 ``repo_path``.
37 37
38 38 :param repo_path: local path of the repository
39 39 :param create=False: if set to True, would try to create repository if
40 40 it does not exist rather than raising exception
41 41 :param baseui=None: user data
42 42 :param src_url=None: would try to clone repository from given location
43 43 :param update_after_clone=False: sets update of working copy after
44 44 making a clone
45 45 """
46 46
47 47 if not isinstance(repo_path, str):
48 48 raise VCSError('Mercurial backend requires repository path to '
49 49 'be instance of <str> got %s instead' %
50 50 type(repo_path))
51 51
52 52 self.path = abspath(repo_path)
53 53 self.baseui = baseui or ui.ui()
54 54 # We've set path and ui, now we can set _repo itself
55 55 self._repo = self._get_repo(create, src_url, update_after_clone)
56 56
57 57 @property
58 58 def _empty(self):
59 59 """
60 60 Checks if repository is empty without any changesets
61 61 """
62 62 # TODO: Following raises errors when using InMemoryChangeset...
63 63 # return len(self._repo.changelog) == 0
64 64 return len(self.revisions) == 0
65 65
66 66 @LazyProperty
67 67 def revisions(self):
68 68 """
69 69 Returns list of revisions' ids, in ascending order. Being lazy
70 70 attribute allows external tools to inject shas from cache.
71 71 """
72 72 return self._get_all_revisions()
73 73
74 74 @LazyProperty
75 75 def name(self):
76 76 return os.path.basename(self.path)
77 77
78 78 @LazyProperty
79 79 def branches(self):
80 80 return self._get_branches()
81 81
82 82 @LazyProperty
83 83 def allbranches(self):
84 84 """
85 85 List all branches, including closed branches.
86 86 """
87 87 return self._get_branches(closed=True)
88 88
89 89 def _get_branches(self, closed=False):
90 90 """
91 91 Get's branches for this repository
92 92 Returns only not closed branches by default
93 93
94 94 :param closed: return also closed branches for mercurial
95 95 """
96 96
97 97 if self._empty:
98 98 return {}
99 99
100 100 def _branchtags(localrepo):
101 101 """
102 102 Patched version of mercurial branchtags to not return the closed
103 103 branches
104 104
105 105 :param localrepo: locarepository instance
106 106 """
107 107
108 108 bt = {}
109 109 bt_closed = {}
110 110 for bn, heads in localrepo.branchmap().iteritems():
111 111 tip = heads[-1]
112 112 if 'close' in localrepo.changelog.read(tip)[5]:
113 113 bt_closed[bn] = tip
114 114 else:
115 115 bt[bn] = tip
116 116
117 117 if closed:
118 118 bt.update(bt_closed)
119 119 return bt
120 120
121 121 sortkey = lambda ctx: ctx[0] # sort by name
122 122 _branches = [(safe_unicode(n), hex(h),) for n, h in
123 123 _branchtags(self._repo).items()]
124 124
125 125 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
126 126
127 127 @LazyProperty
128 128 def tags(self):
129 129 """
130 130 Get's tags for this repository
131 131 """
132 132 return self._get_tags()
133 133
134 134 def _get_tags(self):
135 135 if self._empty:
136 136 return {}
137 137
138 138 sortkey = lambda ctx: ctx[0] # sort by name
139 139 _tags = [(safe_unicode(n), hex(h),) for n, h in
140 140 self._repo.tags().items()]
141 141
142 142 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
143 143
144 144 def tag(self, name, user, revision=None, message=None, date=None,
145 145 **kwargs):
146 146 """
147 147 Creates and returns a tag for the given ``revision``.
148 148
149 149 :param name: name for new tag
150 150 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
151 151 :param revision: changeset id for which new tag would be created
152 152 :param message: message of the tag's commit
153 153 :param date: date of tag's commit
154 154
155 155 :raises TagAlreadyExistError: if tag with same name already exists
156 156 """
157 157 if name in self.tags:
158 158 raise TagAlreadyExistError("Tag %s already exists" % name)
159 159 changeset = self.get_changeset(revision)
160 160 local = kwargs.setdefault('local', False)
161 161
162 162 if message is None:
163 163 message = "Added tag %s for changeset %s" % (name,
164 164 changeset.short_id)
165 165
166 166 if date is None:
167 167 date = datetime.datetime.now().ctime()
168 168
169 169 try:
170 170 self._repo.tag(name, changeset._ctx.node(), message, local, user,
171 171 date)
172 172 except Abort, e:
173 173 raise RepositoryError(e.message)
174 174
175 175 # Reinitialize tags
176 176 self.tags = self._get_tags()
177 177 tag_id = self.tags[name]
178 178
179 179 return self.get_changeset(revision=tag_id)
180 180
181 181 def remove_tag(self, name, user, message=None, date=None):
182 182 """
183 183 Removes tag with the given ``name``.
184 184
185 185 :param name: name of the tag to be removed
186 186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 187 :param message: message of the tag's removal commit
188 188 :param date: date of tag's removal commit
189 189
190 190 :raises TagDoesNotExistError: if tag with given name does not exists
191 191 """
192 192 if name not in self.tags:
193 193 raise TagDoesNotExistError("Tag %s does not exist" % name)
194 194 if message is None:
195 195 message = "Removed tag %s" % name
196 196 if date is None:
197 197 date = datetime.datetime.now().ctime()
198 198 local = False
199 199
200 200 try:
201 201 self._repo.tag(name, nullid, message, local, user, date)
202 202 self.tags = self._get_tags()
203 203 except Abort, e:
204 204 raise RepositoryError(e.message)
205 205
206 206 @LazyProperty
207 207 def bookmarks(self):
208 208 """
209 209 Get's bookmarks for this repository
210 210 """
211 211 return self._get_bookmarks()
212 212
213 213 def _get_bookmarks(self):
214 214 if self._empty:
215 215 return {}
216 216
217 217 sortkey = lambda ctx: ctx[0] # sort by name
218 218 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
219 219 self._repo._bookmarks.items()]
220 220 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
221 221
222 222 def _get_all_revisions(self):
223 223
224 224 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
225 225
226 226 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
227 227 context=3):
228 228 """
229 229 Returns (git like) *diff*, as plain text. Shows changes introduced by
230 230 ``rev2`` since ``rev1``.
231 231
232 232 :param rev1: Entry point from which diff is shown. Can be
233 233 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
234 234 the changes since empty state of the repository until ``rev2``
235 235 :param rev2: Until which revision changes should be shown.
236 236 :param ignore_whitespace: If set to ``True``, would not show whitespace
237 237 changes. Defaults to ``False``.
238 238 :param context: How many lines before/after changed lines should be
239 239 shown. Defaults to ``3``.
240 240 """
241 241 if hasattr(rev1, 'raw_id'):
242 242 rev1 = getattr(rev1, 'raw_id')
243 243
244 244 if hasattr(rev2, 'raw_id'):
245 245 rev2 = getattr(rev2, 'raw_id')
246 246
247 247 # Check if given revisions are present at repository (may raise
248 248 # ChangesetDoesNotExistError)
249 249 if rev1 != self.EMPTY_CHANGESET:
250 250 self.get_changeset(rev1)
251 251 self.get_changeset(rev2)
252 252 if path:
253 253 file_filter = match(self.path, '', [path])
254 254 else:
255 255 file_filter = None
256 256
257 257 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
258 258 opts=diffopts(git=True,
259 259 ignorews=ignore_whitespace,
260 260 context=context)))
261 261
262 262 @classmethod
263 263 def _check_url(cls, url):
264 264 """
265 265 Function will check given url and try to verify if it's a valid
266 266 link. Sometimes it may happened that mercurial will issue basic
267 267 auth request that can cause whole API to hang when used from python
268 268 or other external calls.
269 269
270 270 On failures it'll raise urllib2.HTTPError, return code 200 if url
271 271 is valid or True if it's a local path
272 272 """
273 273
274 274 from mercurial.util import url as Url
275 275
276 276 # those authnadlers are patched for python 2.6.5 bug an
277 277 # infinit looping when given invalid resources
278 278 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
279 279
280 280 # check first if it's not an local url
281 281 if os.path.isdir(url) or url.startswith('file:'):
282 282 return True
283 283
284 284 if('+' in url[:url.find('://')]):
285 285 url = url[url.find('+') + 1:]
286 286
287 287 handlers = []
288 288 test_uri, authinfo = Url(url).authinfo()
289 289
290 290 if authinfo:
291 291 #create a password manager
292 292 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
293 293 passmgr.add_password(*authinfo)
294 294
295 295 handlers.extend((httpbasicauthhandler(passmgr),
296 296 httpdigestauthhandler(passmgr)))
297 297
298 298 o = urllib2.build_opener(*handlers)
299 299 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
300 300 ('Accept', 'application/mercurial-0.1')]
301 301
302 302 q = {"cmd": 'between'}
303 303 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
304 304 qs = '?%s' % urllib.urlencode(q)
305 305 cu = "%s%s" % (test_uri, qs)
306 306 req = urllib2.Request(cu, None, {})
307 307
308 308 try:
309 309 resp = o.open(req)
310 310 return resp.code == 200
311 311 except Exception, e:
312 312 # means it cannot be cloned
313 313 raise urllib2.URLError("[%s] %s" % (url, e))
314 314
315 315 def _get_repo(self, create, src_url=None, update_after_clone=False):
316 316 """
317 317 Function will check for mercurial repository in given path and return
318 318 a localrepo object. If there is no repository in that path it will
319 319 raise an exception unless ``create`` parameter is set to True - in
320 320 that case repository would be created and returned.
321 321 If ``src_url`` is given, would try to clone repository from the
322 322 location at given clone_point. Additionally it'll make update to
323 323 working copy accordingly to ``update_after_clone`` flag
324 324 """
325 325
326 326 try:
327 327 if src_url:
328 328 url = str(self._get_url(src_url))
329 329 opts = {}
330 330 if not update_after_clone:
331 331 opts.update({'noupdate': True})
332 332 try:
333 333 MercurialRepository._check_url(url)
334 334 clone(self.baseui, url, self.path, **opts)
335 335 # except urllib2.URLError:
336 336 # raise Abort("Got HTTP 404 error")
337 337 except Exception:
338 338 raise
339 339
340 340 # Don't try to create if we've already cloned repo
341 341 create = False
342 342 return localrepository(self.baseui, self.path, create=create)
343 343 except (Abort, RepoError), err:
344 344 if create:
345 345 msg = "Cannot create repository at %s. Original error was %s"\
346 346 % (self.path, err)
347 347 else:
348 348 msg = "Not valid repository at %s. Original error was %s"\
349 349 % (self.path, err)
350 350 raise RepositoryError(msg)
351 351
352 352 @LazyProperty
353 353 def in_memory_changeset(self):
354 354 return MercurialInMemoryChangeset(self)
355 355
356 356 @LazyProperty
357 357 def description(self):
358 358 undefined_description = u'unknown'
359 359 return safe_unicode(self._repo.ui.config('web', 'description',
360 360 undefined_description, untrusted=True))
361 361
362 362 @LazyProperty
363 363 def contact(self):
364 364 undefined_contact = u'Unknown'
365 365 return safe_unicode(get_contact(self._repo.ui.config)
366 366 or undefined_contact)
367 367
368 368 @LazyProperty
369 369 def last_change(self):
370 370 """
371 371 Returns last change made on this repository as datetime object
372 372 """
373 373 return date_fromtimestamp(self._get_mtime(), makedate()[1])
374 374
375 375 def _get_mtime(self):
376 376 try:
377 377 return time.mktime(self.get_changeset().date.timetuple())
378 378 except RepositoryError:
379 379 #fallback to filesystem
380 380 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
381 381 st_path = os.path.join(self.path, '.hg', "store")
382 382 if os.path.exists(cl_path):
383 383 return os.stat(cl_path).st_mtime
384 384 else:
385 385 return os.stat(st_path).st_mtime
386 386
387 387 def _get_hidden(self):
388 388 return self._repo.ui.configbool("web", "hidden", untrusted=True)
389 389
390 390 def _get_revision(self, revision):
391 391 """
392 392 Get's an ID revision given as str. This will always return a fill
393 393 40 char revision number
394 394
395 395 :param revision: str or int or None
396 396 """
397 397
398 398 if self._empty:
399 399 raise EmptyRepositoryError("There are no changesets yet")
400 400
401 401 if revision in [-1, 'tip', None]:
402 402 revision = 'tip'
403 403
404 404 try:
405 405 revision = hex(self._repo.lookup(revision))
406 406 except (IndexError, ValueError, RepoLookupError, TypeError):
407 raise ChangesetDoesNotExistError("Revision %r does not "
407 raise ChangesetDoesNotExistError("Revision %s does not "
408 408 "exist for this repository"
409 409 % (revision))
410 410 return revision
411 411
412 412 def _get_archives(self, archive_name='tip'):
413 413 allowed = self.baseui.configlist("web", "allow_archive",
414 414 untrusted=True)
415 415 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
416 416 if i[0] in allowed or self._repo.ui.configbool("web",
417 417 "allow" + i[0],
418 418 untrusted=True):
419 419 yield {"type": i[0], "extension": i[1], "node": archive_name}
420 420
421 421 def _get_url(self, url):
422 422 """
423 423 Returns normalized url. If schema is not given, would fall
424 424 to filesystem
425 425 (``file:///``) schema.
426 426 """
427 427 url = str(url)
428 428 if url != 'default' and not '://' in url:
429 429 url = "file:" + urllib.pathname2url(url)
430 430 return url
431 431
432 432 def get_hook_location(self):
433 433 """
434 434 returns absolute path to location where hooks are stored
435 435 """
436 436 return os.path.join(self.path, '.hg', '.hgrc')
437 437
438 438 def get_changeset(self, revision=None):
439 439 """
440 440 Returns ``MercurialChangeset`` object representing repository's
441 441 changeset at the given ``revision``.
442 442 """
443 443 revision = self._get_revision(revision)
444 444 changeset = MercurialChangeset(repository=self, revision=revision)
445 445 return changeset
446 446
447 447 def get_changesets(self, start=None, end=None, start_date=None,
448 448 end_date=None, branch_name=None, reverse=False):
449 449 """
450 450 Returns iterator of ``MercurialChangeset`` objects from start to end
451 451 (both are inclusive)
452 452
453 453 :param start: None, str, int or mercurial lookup format
454 454 :param end: None, str, int or mercurial lookup format
455 455 :param start_date:
456 456 :param end_date:
457 457 :param branch_name:
458 458 :param reversed: return changesets in reversed order
459 459 """
460 460
461 461 start_raw_id = self._get_revision(start)
462 462 start_pos = self.revisions.index(start_raw_id) if start else None
463 463 end_raw_id = self._get_revision(end)
464 464 end_pos = self.revisions.index(end_raw_id) if end else None
465 465
466 466 if None not in [start, end] and start_pos > end_pos:
467 467 raise RepositoryError("Start revision '%s' cannot be "
468 468 "after end revision '%s'" % (start, end))
469 469
470 470 if branch_name and branch_name not in self.allbranches.keys():
471 471 raise BranchDoesNotExistError('Branch %s not found in'
472 472 ' this repository' % branch_name)
473 473 if end_pos is not None:
474 474 end_pos += 1
475 475
476 476 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
477 477 self.revisions[start_pos:end_pos]
478 478
479 479 for id_ in slice_:
480 480 cs = self.get_changeset(id_)
481 481 if branch_name and cs.branch != branch_name:
482 482 continue
483 483 if start_date and cs.date < start_date:
484 484 continue
485 485 if end_date and cs.date > end_date:
486 486 continue
487 487
488 488 yield cs
489 489
490 490 def pull(self, url):
491 491 """
492 492 Tries to pull changes from external location.
493 493 """
494 494 url = self._get_url(url)
495 495 try:
496 496 pull(self.baseui, self._repo, url)
497 497 except Abort, err:
498 498 # Propagate error but with vcs's type
499 499 raise RepositoryError(str(err))
500 500
501 501 @LazyProperty
502 502 def workdir(self):
503 503 """
504 504 Returns ``Workdir`` instance for this repository.
505 505 """
506 506 return MercurialWorkdir(self)
507 507
508 508 def get_config_value(self, section, name=None, config_file=None):
509 509 """
510 510 Returns configuration value for a given [``section``] and ``name``.
511 511
512 512 :param section: Section we want to retrieve value from
513 513 :param name: Name of configuration we want to retrieve
514 514 :param config_file: A path to file which should be used to retrieve
515 515 configuration from (might also be a list of file paths)
516 516 """
517 517 if config_file is None:
518 518 config_file = []
519 519 elif isinstance(config_file, basestring):
520 520 config_file = [config_file]
521 521
522 522 config = self._repo.ui
523 523 for path in config_file:
524 524 config.readconfig(path)
525 525 return config.config(section, name)
526 526
527 527 def get_user_name(self, config_file=None):
528 528 """
529 529 Returns user's name from global configuration file.
530 530
531 531 :param config_file: A path to file which should be used to retrieve
532 532 configuration from (might also be a list of file paths)
533 533 """
534 534 username = self.get_config_value('ui', 'username')
535 535 if username:
536 536 return author_name(username)
537 537 return None
538 538
539 539 def get_user_email(self, config_file=None):
540 540 """
541 541 Returns user's email from global configuration file.
542 542
543 543 :param config_file: A path to file which should be used to retrieve
544 544 configuration from (might also be a list of file paths)
545 545 """
546 546 username = self.get_config_value('ui', 'username')
547 547 if username:
548 548 return author_email(username)
549 549 return None
@@ -1,355 +1,355
1 1 from rhodecode.tests import *
2 2 from rhodecode.model.db import Repository
3 3 from rhodecode.model.meta import Session
4 4
5 5 ARCHIVE_SPECS = {
6 6 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
7 7 '.tar.gz': ('application/x-gzip', 'tgz', ''),
8 8 '.zip': ('application/zip', 'zip', ''),
9 9 }
10 10
11 11
12 12 def _set_downloads(repo_name, set_to):
13 13 repo = Repository.get_by_repo_name(repo_name)
14 14 repo.enable_downloads = set_to
15 15 Session().add(repo)
16 16 Session().commit()
17 17
18 18
19 19 class TestFilesController(TestController):
20 20
21 21 def test_index(self):
22 22 self.log_user()
23 23 response = self.app.get(url(controller='files', action='index',
24 24 repo_name=HG_REPO,
25 25 revision='tip',
26 26 f_path='/'))
27 27 # Test response...
28 28 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
29 29 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
30 30 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
31 31 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
32 32 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
33 33
34 34 def test_index_revision(self):
35 35 self.log_user()
36 36
37 37 response = self.app.get(
38 38 url(controller='files', action='index',
39 39 repo_name=HG_REPO,
40 40 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
41 41 f_path='/')
42 42 )
43 43
44 44 #Test response...
45 45
46 46 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
47 47 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
48 48 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
49 49 response.mustcontain('1.1 KiB')
50 50 response.mustcontain('text/x-python')
51 51
52 52 def test_index_different_branch(self):
53 53 self.log_user()
54 54
55 55 response = self.app.get(url(controller='files', action='index',
56 56 repo_name=HG_REPO,
57 57 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
58 58 f_path='/'))
59 59
60 60 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
61 61
62 62 def test_index_paging(self):
63 63 self.log_user()
64 64
65 65 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
66 66 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
67 67 (109, '75feb4c33e81186c87eac740cee2447330288412'),
68 68 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
69 69 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
70 70
71 71 response = self.app.get(url(controller='files', action='index',
72 72 repo_name=HG_REPO,
73 73 revision=r[1],
74 74 f_path='/'))
75 75
76 76 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
77 77
78 78 def test_file_source(self):
79 79 self.log_user()
80 80 response = self.app.get(url(controller='files', action='index',
81 81 repo_name=HG_REPO,
82 82 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
83 83 f_path='vcs/nodes.py'))
84 84
85 85 response.mustcontain("""<div class="commit">Partially implemented <a class="issue-tracker-link" href="https://myissueserver.com/vcs_test_hg/issue/16">#16</a>. filecontent/commit message/author/node name are safe_unicode now.
86 86 In addition some other __str__ are unicode as well
87 87 Added test for unicode
88 88 Improved test to clone into uniq repository.
89 89 removed extra unicode conversion in diff.</div>
90 90 """)
91 91
92 92 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
93 93
94 94 def test_file_source_history(self):
95 95 self.log_user()
96 96 response = self.app.get(url(controller='files', action='history',
97 97 repo_name=HG_REPO,
98 98 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
99 99 f_path='vcs/nodes.py'),
100 100 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
101 101 #test or history
102 102 response.mustcontain("""<optgroup label="Changesets">
103 103 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
104 104 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
105 105 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
106 106 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
107 107 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
108 108 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
109 109 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
110 110 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
111 111 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
112 112 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
113 113 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
114 114 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
115 115 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
116 116 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
117 117 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
118 118 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
119 119 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
120 120 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
121 121 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
122 122 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
123 123 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
124 124 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
125 125 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
126 126 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
127 127 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
128 128 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
129 129 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
130 130 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
131 131 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
132 132 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
133 133 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
134 134 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
135 135 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
136 136 </optgroup>
137 137 <optgroup label="Branches">
138 138 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
139 139 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
140 140 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
141 141 </optgroup>
142 142 <optgroup label="Tags">
143 143 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
144 144 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
145 145 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
146 146 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
147 147 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
148 148 </optgroup>
149 149 """)
150 150
151 151 def test_file_annotation(self):
152 152 self.log_user()
153 153 response = self.app.get(url(controller='files', action='index',
154 154 repo_name=HG_REPO,
155 155 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
156 156 f_path='vcs/nodes.py',
157 157 annotate=True))
158 158
159 159 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
160 160
161 161 def test_file_annotation_history(self):
162 162 self.log_user()
163 163 response = self.app.get(url(controller='files', action='history',
164 164 repo_name=HG_REPO,
165 165 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
166 166 f_path='vcs/nodes.py',
167 167 annotate=True),
168 168 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
169 169
170 170 response.mustcontain("""<optgroup label="Changesets">
171 171 <option selected="selected" value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
172 172 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
173 173 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
174 174 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
175 175 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
176 176 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
177 177 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
178 178 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
179 179 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
180 180 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
181 181 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
182 182 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
183 183 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
184 184 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
185 185 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
186 186 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
187 187 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
188 188 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
189 189 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
190 190 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
191 191 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
192 192 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
193 193 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
194 194 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
195 195 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
196 196 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
197 197 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
198 198 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
199 199 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
200 200 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
201 201 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
202 202 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
203 203 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
204 204 </optgroup>
205 205 <optgroup label="Branches">
206 206 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
207 207 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
208 208 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
209 209 </optgroup>
210 210 <optgroup label="Tags">
211 211 <option value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
212 212 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
213 213 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
214 214 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
215 215 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
216 216 </optgroup>""")
217 217
218 218 def test_file_annotation_git(self):
219 219 self.log_user()
220 220 response = self.app.get(url(controller='files', action='index',
221 221 repo_name=GIT_REPO,
222 222 revision='master',
223 223 f_path='vcs/nodes.py',
224 224 annotate=True))
225 225
226 226 def test_archival(self):
227 227 self.log_user()
228 228 _set_downloads(HG_REPO, set_to=True)
229 229 for arch_ext, info in ARCHIVE_SPECS.items():
230 230 short = '27cd5cce30c9%s' % arch_ext
231 231 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
232 232 filename = '%s-%s' % (HG_REPO, short)
233 233 response = self.app.get(url(controller='files',
234 234 action='archivefile',
235 235 repo_name=HG_REPO,
236 236 fname=fname))
237 237
238 238 self.assertEqual(response.status, '200 OK')
239 239 heads = [
240 240 ('Pragma', 'no-cache'),
241 241 ('Cache-Control', 'no-cache'),
242 242 ('Content-Disposition', 'attachment; filename=%s' % filename),
243 243 ('Content-Type', '%s; charset=utf-8' % info[0]),
244 244 ]
245 245 self.assertEqual(response.response._headers.items(), heads)
246 246
247 247 def test_archival_wrong_ext(self):
248 248 self.log_user()
249 249 _set_downloads(HG_REPO, set_to=True)
250 250 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
251 251 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
252 252
253 253 response = self.app.get(url(controller='files',
254 254 action='archivefile',
255 255 repo_name=HG_REPO,
256 256 fname=fname))
257 257 response.mustcontain('Unknown archive type')
258 258
259 259 def test_archival_wrong_revision(self):
260 260 self.log_user()
261 261 _set_downloads(HG_REPO, set_to=True)
262 262 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
263 263 fname = '%s.zip' % rev
264 264
265 265 response = self.app.get(url(controller='files',
266 266 action='archivefile',
267 267 repo_name=HG_REPO,
268 268 fname=fname))
269 269 response.mustcontain('Unknown revision')
270 270
271 271 #==========================================================================
272 272 # RAW FILE
273 273 #==========================================================================
274 274 def test_raw_file_ok(self):
275 275 self.log_user()
276 276 response = self.app.get(url(controller='files', action='rawfile',
277 277 repo_name=HG_REPO,
278 278 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
279 279 f_path='vcs/nodes.py'))
280 280
281 281 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
282 282 self.assertEqual(response.content_type, "text/x-python")
283 283
284 284 def test_raw_file_wrong_cs(self):
285 285 self.log_user()
286 286 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
287 287 f_path = 'vcs/nodes.py'
288 288
289 289 response = self.app.get(url(controller='files', action='rawfile',
290 290 repo_name=HG_REPO,
291 291 revision=rev,
292 292 f_path=f_path))
293 293
294 msg = """Revision %r does not exist for this repository""" % (rev)
294 msg = """Revision %s does not exist for this repository""" % (rev)
295 295 self.checkSessionFlash(response, msg)
296 296
297 297 self.assertEqual('http://localhost/%s/files/tip/' % HG_REPO, response.headers['location'])
298 298
299 299 def test_raw_file_wrong_f_path(self):
300 300 self.log_user()
301 301 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
302 302 f_path = 'vcs/ERRORnodes.py'
303 303 response = self.app.get(url(controller='files', action='rawfile',
304 304 repo_name=HG_REPO,
305 305 revision=rev,
306 306 f_path=f_path))
307 307
308 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
308 msg = "There is no file nor directory at the given path: '%s' at revision %s" % (f_path, rev[:12])
309 309 self.checkSessionFlash(response, msg)
310 310
311 311 #==========================================================================
312 312 # RAW RESPONSE - PLAIN
313 313 #==========================================================================
314 314 def test_raw_ok(self):
315 315 self.log_user()
316 316 response = self.app.get(url(controller='files', action='raw',
317 317 repo_name=HG_REPO,
318 318 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
319 319 f_path='vcs/nodes.py'))
320 320
321 321 self.assertEqual(response.content_type, "text/plain")
322 322
323 323 def test_raw_wrong_cs(self):
324 324 self.log_user()
325 325 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
326 326 f_path = 'vcs/nodes.py'
327 327
328 328 response = self.app.get(url(controller='files', action='raw',
329 329 repo_name=HG_REPO,
330 330 revision=rev,
331 331 f_path=f_path))
332 msg = """Revision %r does not exist for this repository""" % (rev)
332 msg = """Revision %s does not exist for this repository""" % (rev)
333 333 self.checkSessionFlash(response, msg)
334 334
335 335 self.assertEqual('http://localhost/%s/files/tip/' % HG_REPO, response.headers['location'])
336 336
337 337 def test_raw_wrong_f_path(self):
338 338 self.log_user()
339 339 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
340 340 f_path = 'vcs/ERRORnodes.py'
341 341 response = self.app.get(url(controller='files', action='raw',
342 342 repo_name=HG_REPO,
343 343 revision=rev,
344 344 f_path=f_path))
345 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
345 msg = "There is no file nor directory at the given path: '%s' at revision %s" % (f_path, rev[:12])
346 346 self.checkSessionFlash(response, msg)
347 347
348 348 def test_ajaxed_files_list(self):
349 349 self.log_user()
350 350 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
351 351 response = self.app.get(
352 352 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
353 353 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
354 354 )
355 355 response.mustcontain("vcs/web/simplevcs/views/repository.py")
General Comments 0
You need to be logged in to leave comments. Login now