├── vcs ├── conf │ ├── __init__.py │ └── settings.py ├── commands │ ├── __init__.py │ ├── standup.py │ ├── completion.py │ ├── log.py │ ├── summary.py │ └── cat.py ├── tests │ ├── __main__.py │ ├── aconfig │ ├── test_utils_filesize.py │ ├── test_filenodes_unicode_path.py │ ├── test_getitem.py │ ├── test_getslice.py │ ├── conf.py │ ├── test_tags.py │ ├── test_diffs.py │ ├── __init__.py │ ├── test_vcs.py │ ├── utils.py │ ├── test_workdirs.py │ ├── test_archives.py │ ├── base.py │ ├── test_branches.py │ ├── test_repository.py │ ├── test_nodes.py │ ├── test_cli.py │ ├── test_utils_progressbar.py │ └── test_utils.py ├── backends │ ├── git │ │ ├── __init__.py │ │ ├── workdir.py │ │ └── inmemory.py │ ├── hg │ │ ├── __init__.py │ │ ├── workdir.py │ │ └── inmemory.py │ └── __init__.py ├── utils │ ├── fakemod.py │ ├── imports.py │ ├── filesize.py │ ├── hgcompat.py │ ├── paths.py │ ├── archivers.py │ ├── baseui_config.py │ ├── lazy.py │ ├── lockfiles.py │ ├── ordered_dict.py │ ├── __init__.py │ ├── termcolors.py │ ├── annotate.py │ └── helpers.py ├── __init__.py └── exceptions.py ├── docs ├── license.rst ├── requirements.txt ├── theme │ └── ADC │ │ ├── static │ │ ├── scrn1.png │ │ ├── scrn2.png │ │ ├── documentation.png │ │ ├── header_sm_mid.png │ │ ├── triangle_left.png │ │ ├── triangle_open.png │ │ ├── title_background.png │ │ ├── triangle_closed.png │ │ ├── searchfield_leftcap.png │ │ ├── searchfield_repeat.png │ │ ├── breadcrumb_background.png │ │ ├── searchfield_rightcap.png │ │ ├── mobile.css │ │ └── toc.js │ │ ├── theme.conf │ │ └── layout.html ├── usage │ ├── index.rst │ └── vcsrc.rst ├── api │ ├── index.rst │ ├── backends │ │ ├── index.rst │ │ ├── git.rst │ │ └── hg.rst │ ├── nodes.rst │ ├── utils │ │ └── index.rst │ ├── cli.rst │ └── conf.settings.rst ├── exts.py ├── alternatives.rst ├── index.rst ├── contribute.rst ├── installation.rst ├── Makefile ├── make.bat ├── conf.py └── quickstart.rst ├── .gitignore ├── MANIFEST.in ├── setup.cfg ├── run_test_and_report.sh ├── test_and_report.sh ├── .hgignore ├── tox.ini ├── .travis.yml ├── .hgtags ├── LICENSE ├── README.rst ├── setup.py └── extras.py /vcs/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vcs/commands/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | registry = {} 3 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | License 4 | ======= 5 | 6 | .. literalinclude:: ../LICENSE 7 | 8 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # for https://readthedocs.org/ 2 | mock 3 | pygments 4 | dulwich 5 | mercurial 6 | -------------------------------------------------------------------------------- /docs/theme/ADC/static/scrn1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/scrn1.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/scrn2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/scrn2.png -------------------------------------------------------------------------------- /docs/theme/ADC/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = adctheme.css 4 | pygments_style = friendly 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | .coverage 4 | .hg 5 | .ropeproject 6 | *.egg 7 | 8 | build 9 | vcs.egg-info 10 | .tox 11 | -------------------------------------------------------------------------------- /docs/theme/ADC/static/documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/documentation.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/header_sm_mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/header_sm_mid.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/triangle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/triangle_left.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/triangle_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/triangle_open.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/title_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/title_background.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/triangle_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/triangle_closed.png -------------------------------------------------------------------------------- /docs/usage/index.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Usage 4 | ===== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | vcsrc 10 | -------------------------------------------------------------------------------- /docs/theme/ADC/static/searchfield_leftcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/searchfield_leftcap.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/searchfield_repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/searchfield_repeat.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/breadcrumb_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/breadcrumb_background.png -------------------------------------------------------------------------------- /docs/theme/ADC/static/searchfield_rightcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeinn/vcs/HEAD/docs/theme/ADC/static/searchfield_rightcap.png -------------------------------------------------------------------------------- /vcs/tests/__main__.py: -------------------------------------------------------------------------------- 1 | from __init__ import * # allows to run tests with: python vcs/tests 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include MANIFEST.in 3 | recursive-include vcs * 4 | recursive-include docs * 5 | recursive-exclude docs/build * 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/build/html 8 | 9 | -------------------------------------------------------------------------------- /vcs/tests/aconfig: -------------------------------------------------------------------------------- 1 | [user] 2 | name = Foo Bar 3 | email = foo.bar@example.com 4 | 5 | [ui] 6 | username = Foo Bar foo.bar@example.com 7 | 8 | [universal] 9 | foo = bar 10 | 11 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Reference 4 | ============= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | cli 10 | conf.settings 11 | nodes 12 | backends/index 13 | utils/index 14 | -------------------------------------------------------------------------------- /run_test_and_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running test suite with coverage report at the end" 4 | echo -e "( would require coverage python package to be installed )\n" 5 | 6 | coverage run setup.py test 7 | coverage report -m --omit=setup,tests,vcs/web/simplevcs 8 | 9 | -------------------------------------------------------------------------------- /test_and_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running test suite with coverage report at the end" 4 | echo -e "( would require coverage python package to be installed )\n" 5 | 6 | coverage run setup.py test 7 | coverage report -m --omit=example_project,setup,tests,vcs/web/simplevcs 8 | 9 | -------------------------------------------------------------------------------- /vcs/backends/git/__init__.py: -------------------------------------------------------------------------------- 1 | from .repository import GitRepository 2 | from .changeset import GitChangeset 3 | from .inmemory import GitInMemoryChangeset 4 | from .workdir import GitWorkdir 5 | 6 | 7 | __all__ = [ 8 | 'GitRepository', 'GitChangeset', 'GitInMemoryChangeset', 'GitWorkdir', 9 | ] 10 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: regexp 2 | ^\.settings$ 3 | syntax: regexp 4 | ^\.project$ 5 | syntax: regexp 6 | ^\.pydevproject$ 7 | syntax: glob 8 | *.pyc 9 | *.orig 10 | *.log 11 | *.egg 12 | *.egg-info 13 | *.swp 14 | *.bak 15 | *.db 16 | *.tox 17 | .coverage 18 | pip-log.txt 19 | dist 20 | .DS_Store 21 | .coverage 22 | build 23 | .ropeproject 24 | -------------------------------------------------------------------------------- /docs/exts.py: -------------------------------------------------------------------------------- 1 | 2 | def setup(app): 3 | app.add_crossref_type( 4 | directivename = "command", 5 | rolename = "command", 6 | indextemplate = "pair: %s; command", 7 | ) 8 | app.add_crossref_type( 9 | directivename = "setting", 10 | rolename = "setting", 11 | indextemplate = "pair: %s; setting", 12 | ) 13 | -------------------------------------------------------------------------------- /docs/api/backends/index.rst: -------------------------------------------------------------------------------- 1 | .. _api-backends: 2 | 3 | vcs.backends 4 | ============ 5 | 6 | .. module:: vcs.backends 7 | 8 | Implemented Backends 9 | -------------------- 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | git 15 | hg 16 | 17 | .. _api-base-backend: 18 | 19 | Base Backend 20 | ------------ 21 | 22 | .. automodule:: vcs.backends.base 23 | :members: 24 | -------------------------------------------------------------------------------- /vcs/backends/hg/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .repository import MercurialRepository 3 | from .changeset import MercurialChangeset 4 | from .inmemory import MercurialInMemoryChangeset 5 | from .workdir import MercurialWorkdir 6 | 7 | 8 | __all__ = [ 9 | 'MercurialRepository', 'MercurialChangeset', 10 | 'MercurialInMemoryChangeset', 'MercurialWorkdir', 11 | ] 12 | -------------------------------------------------------------------------------- /vcs/commands/standup.py: -------------------------------------------------------------------------------- 1 | from vcs.commands.log import LogCommand 2 | 3 | 4 | class StandupCommand(LogCommand): 5 | 6 | def handle_repo(self, repo, **options): 7 | options['all'] = True 8 | options['start_date'] = '1day' 9 | username = repo.get_user_name() 10 | options['author'] = username + '*' 11 | return super(StandupCommand, self).handle_repo(repo, **options) 12 | -------------------------------------------------------------------------------- /docs/theme/ADC/static/mobile.css: -------------------------------------------------------------------------------- 1 | /* 2 | * CSS adjustments (overrides) for mobile browsers that cannot handle 3 | * fix-positioned div's very well. 4 | * This makes long pages scrollable on mobile browsers. 5 | */ 6 | 7 | #breadcrumbs { 8 | display: none !important; 9 | } 10 | 11 | .document { 12 | bottom: inherit !important; 13 | } 14 | 15 | #sphinxsidebar { 16 | bottom: inherit !important; 17 | } 18 | -------------------------------------------------------------------------------- /vcs/utils/fakemod.py: -------------------------------------------------------------------------------- 1 | import imp 2 | 3 | 4 | def create_module(name, path): 5 | """ 6 | Returns module created *on the fly*. Returned module would have name same 7 | as given ``name`` and would contain code read from file at the given 8 | ``path`` (it may also be a zip or package containing *__main__* module). 9 | """ 10 | module = imp.new_module(name) 11 | module.__file__ = path 12 | execfile(path, module.__dict__) 13 | return module 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py25,py26,py27,docs,flakes 3 | 4 | [testenv] 5 | commands = python setup.py test 6 | deps = 7 | setuptools==17.1 8 | 9 | [testenv:docs] 10 | changedir = docs 11 | deps = 12 | sphinx 13 | dulwich 14 | mercurial 15 | pygments 16 | unittest2 17 | commands = 18 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 19 | 20 | [testenv:flakes] 21 | deps = 22 | pyflakes >= 0.6 23 | commands = pyflakes ./vcs 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | 5 | env: 6 | - TOX_ENV=py25 7 | - TOX_ENV=py26 8 | - TOX_ENV=py27 9 | 10 | install: 11 | pip install tox 12 | 13 | script: tox -e $TOX_ENV 14 | 15 | # flakes show some unused imports and start imports, so we don't want to have red build till they are fixed 16 | after_script: tox -e flakes 17 | 18 | notifications: 19 | email: 20 | - lukaszbalcerzak@gmail.com 21 | - marcinkuz@gmail.com 22 | irc: "irc.freenode.org#vcs" 23 | -------------------------------------------------------------------------------- /docs/api/nodes.rst: -------------------------------------------------------------------------------- 1 | .. _api-nodes: 2 | 3 | vcs.nodes 4 | ========= 5 | 6 | .. module:: vcs.nodes 7 | 8 | Node 9 | ---- 10 | 11 | .. autoclass:: vcs.nodes.Node 12 | :members: 13 | 14 | FileNode 15 | -------- 16 | 17 | .. autoclass:: vcs.nodes.FileNode 18 | :members: 19 | 20 | RemovedFileNode 21 | --------------- 22 | 23 | .. autoclass:: vcs.nodes.RemovedFileNode 24 | :members: 25 | 26 | DirNode 27 | ------- 28 | 29 | .. autoclass:: vcs.nodes.DirNode 30 | :members: 31 | 32 | RootNode 33 | -------- 34 | 35 | .. autoclass:: vcs.nodes.RootNode 36 | :members: 37 | -------------------------------------------------------------------------------- /docs/theme/ADC/static/toc.js: -------------------------------------------------------------------------------- 1 | var TOC = { 2 | load: function () { 3 | $('#toc_button').click(TOC.toggle); 4 | }, 5 | 6 | toggle: function () { 7 | if ($('#sphinxsidebar').toggle().is(':hidden')) { 8 | $('div.document').css('left', "0px"); 9 | $('toc_button').removeClass("open"); 10 | } else { 11 | $('div.document').css('left', "230px"); 12 | $('#toc_button').addClass("open"); 13 | } 14 | return $('#sphinxsidebar'); 15 | } 16 | }; 17 | 18 | $(document).ready(function () { 19 | TOC.load(); 20 | }); -------------------------------------------------------------------------------- /vcs/backends/hg/workdir.py: -------------------------------------------------------------------------------- 1 | from vcs.backends.base import BaseWorkdir 2 | from vcs.exceptions import BranchDoesNotExistError 3 | 4 | from vcs.utils.hgcompat import hg_merge 5 | 6 | 7 | class MercurialWorkdir(BaseWorkdir): 8 | 9 | def get_branch(self): 10 | return self.repository._repo.dirstate.branch() 11 | 12 | def get_changeset(self): 13 | wk_dir_id = self.repository._repo[None].parents()[0].hex() 14 | return self.repository.get_changeset(wk_dir_id) 15 | 16 | def checkout_branch(self, branch=None): 17 | if branch is None: 18 | branch = self.repository.DEFAULT_BRANCH_NAME 19 | if branch not in self.repository.branches: 20 | raise BranchDoesNotExistError 21 | 22 | hg_merge.update(self.repository._repo, branch, False, False, None) 23 | -------------------------------------------------------------------------------- /vcs/tests/test_utils_filesize.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from vcs.utils.filesize import filesizeformat 4 | from vcs.utils.compat import unittest 5 | 6 | 7 | class TestFilesizeformat(unittest.TestCase): 8 | 9 | def test_bytes(self): 10 | self.assertEqual(filesizeformat(10), '10 B') 11 | 12 | def test_kilobytes(self): 13 | self.assertEqual(filesizeformat(1024 * 2), '2 KB') 14 | 15 | def test_megabytes(self): 16 | self.assertEqual(filesizeformat(1024 * 1024 * 2.3), '2.3 MB') 17 | 18 | def test_gigabytes(self): 19 | self.assertEqual(filesizeformat(1024 * 1024 * 1024 * 12.92), '12.92 GB') 20 | 21 | def test_that_function_respects_sep_paramtere(self): 22 | self.assertEqual(filesizeformat(1, ''), '1B') 23 | 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /docs/api/utils/index.rst: -------------------------------------------------------------------------------- 1 | .. _api-utils: 2 | 3 | vcs.utils 4 | ========= 5 | 6 | .. automodule:: vcs.utils 7 | 8 | **Public API** 9 | 10 | * :ref:`api-utils-annotate` 11 | * :ref:`api-utils-diffs` 12 | * :ref:`api-utils-helpers` 13 | 14 | **Private API** 15 | 16 | * :ref:`api-utils-lazy` 17 | 18 | 19 | .. _api-utils-annotate: 20 | 21 | Annotate utils 22 | -------------- 23 | 24 | .. automodule:: vcs.utils.annotate 25 | :members: 26 | 27 | 28 | .. _api-utils-diffs: 29 | 30 | Diffs utils 31 | ----------- 32 | 33 | .. automodule:: vcs.utils.diffs 34 | :members: 35 | 36 | 37 | .. _api-utils-helpers: 38 | 39 | Helpers 40 | ------- 41 | 42 | .. automodule:: vcs.utils.helpers 43 | :members: 44 | 45 | 46 | .. _api-utils-lazy: 47 | 48 | Lazy attributes utils 49 | --------------------- 50 | 51 | .. automodule:: vcs.utils.lazy 52 | :members: 53 | -------------------------------------------------------------------------------- /docs/alternatives.rst: -------------------------------------------------------------------------------- 1 | .. _alternatives: 2 | 3 | Alternatives 4 | ------------ 5 | 6 | There are a couple of alternatives to vcs: 7 | 8 | - `anyvc `_ actively maintained, similar to 9 | vcs (in a way it tries to abstract scms), supports more backends (svn, bzr); 10 | as far as we can tell it's main heart of Pida_; it's main focus however is on 11 | working directories, does not support in memory commits or history 12 | traversing; 13 | 14 | - `pyvcs `_ not actively maintained; this 15 | package focus on history and repository traversing, does not support commits 16 | at all; is much simpler from vcs so may be used if you don't need full repos 17 | interface 18 | 19 | .. note:: 20 | If you know any other similar Python library, please let us know! 21 | 22 | .. _pida: http://pida.co.uk/ 23 | -------------------------------------------------------------------------------- /vcs/utils/imports.py: -------------------------------------------------------------------------------- 1 | from vcs.exceptions import VCSError 2 | 3 | 4 | def import_class(class_path): 5 | """ 6 | Returns class from the given path. 7 | 8 | For example, in order to get class located at 9 | ``vcs.backends.hg.MercurialRepository``: 10 | 11 | try: 12 | hgrepo = import_class('vcs.backends.hg.MercurialRepository') 13 | except VCSError: 14 | # hadle error 15 | """ 16 | splitted = class_path.split('.') 17 | mod_path = '.'.join(splitted[:-1]) 18 | class_name = splitted[-1] 19 | try: 20 | class_mod = __import__(mod_path, {}, {}, [class_name]) 21 | except ImportError, err: 22 | msg = "There was problem while trying to import backend class. "\ 23 | "Original error was:\n%s" % err 24 | raise VCSError(msg) 25 | cls = getattr(class_mod, class_name) 26 | 27 | return cls 28 | -------------------------------------------------------------------------------- /vcs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | VERSION = (0, 5, 0, 'dev') 4 | 5 | __version__ = '.'.join((str(each) for each in VERSION[:4])) 6 | 7 | __all__ = [ 8 | 'get_version', 'get_repo', 'get_backend', 9 | 'VCSError', 'RepositoryError', 'ChangesetError' 10 | ] 11 | 12 | import sys 13 | from vcs.backends import get_repo, get_backend 14 | from vcs.exceptions import VCSError, RepositoryError, ChangesetError 15 | 16 | 17 | def get_version(): 18 | """ 19 | Returns shorter version (digit parts only) as string. 20 | """ 21 | return '.'.join((str(each) for each in VERSION[:3])) 22 | 23 | 24 | def main(argv=None): 25 | if argv is None: 26 | argv = sys.argv 27 | from vcs.cli import ExecutionManager 28 | manager = ExecutionManager(argv) 29 | manager.execute() 30 | return 0 31 | 32 | if __name__ == '__main__': 33 | sys.exit(main(sys.argv)) 34 | -------------------------------------------------------------------------------- /vcs/utils/filesize.py: -------------------------------------------------------------------------------- 1 | def filesizeformat(bytes, sep=' '): 2 | """ 3 | Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 4 | 102 B, 2.3 GB etc). 5 | 6 | Grabbed from Django (http://www.djangoproject.com), slightly modified. 7 | 8 | :param bytes: size in bytes (as integer) 9 | :param sep: string separator between number and abbreviation 10 | """ 11 | try: 12 | bytes = float(bytes) 13 | except (TypeError, ValueError, UnicodeDecodeError): 14 | return '0%sB' % sep 15 | 16 | if bytes < 1024: 17 | size = bytes 18 | template = '%.0f%sB' 19 | elif bytes < 1024 * 1024: 20 | size = bytes / 1024 21 | template = '%.0f%sKB' 22 | elif bytes < 1024 * 1024 * 1024: 23 | size = bytes / 1024 / 1024 24 | template = '%.1f%sMB' 25 | else: 26 | size = bytes / 1024 / 1024 / 1024 27 | template = '%.2f%sGB' 28 | return template % (size, sep) 29 | -------------------------------------------------------------------------------- /vcs/utils/hgcompat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mercurial libs compatibility 3 | """ 4 | 5 | from mercurial import archival, merge as hg_merge, patch, ui 6 | from mercurial.commands import clone, nullid, pull 7 | from mercurial.context import memctx, memfilectx 8 | from mercurial.error import RepoError, RepoLookupError, Abort 9 | from mercurial.hgweb.common import get_contact 10 | from mercurial.localrepo import localrepository 11 | from mercurial.match import match 12 | from mercurial.mdiff import diffopts 13 | from mercurial.node import hex 14 | from mercurial.encoding import tolocal 15 | from mercurial import discovery 16 | from mercurial import localrepo 17 | from mercurial import scmutil 18 | from mercurial.discovery import findcommonoutgoing 19 | 20 | from mercurial.util import url as hg_url 21 | 22 | # those authnadlers are patched for python 2.6.5 bug an 23 | # infinit looping when given invalid resources 24 | from mercurial.url import httpbasicauthhandler, httpdigestauthhandler 25 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | eb3a60fc964309c1a318b8dfe26aa2d1586c85ae v0.1.1 2 | a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720 v0.1.2 3 | 17544fbfcd33ffb439e2b728b5d526b1ef30bfcf v0.1.3 4 | fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200 v0.1.4 5 | a6664e18181c6fc81b751a8d01474e7e1a3fe7fc v0.1.5 6 | 02b38c0eb6f982174750c0e309ff9faddc0c7e12 v0.1.6 7 | fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200 0.1.4 8 | 0000000000000000000000000000000000000000 0.1.4 9 | 17544fbfcd33ffb439e2b728b5d526b1ef30bfcf 0.1.3 10 | 0000000000000000000000000000000000000000 0.1.3 11 | a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720 0.1.2 12 | 0000000000000000000000000000000000000000 0.1.2 13 | eb3a60fc964309c1a318b8dfe26aa2d1586c85ae 0.1.1 14 | 0000000000000000000000000000000000000000 0.1.1 15 | f67633a2894edaf28513706d558205fa93df9209 v0.1.7 16 | ecb25ba9c96faf1e65a0bc3fd914918420a2f116 v0.1.8 17 | 8680b1d1cee3aa3c1ab3734b76ee164bbedbc5c9 v0.1.9 18 | 92831aebf2f8dd4879e897024b89d09af214df1c v0.1.10 19 | 2c96c02def9a7c997f33047761a53943e6254396 v0.2.0 20 | -------------------------------------------------------------------------------- /docs/api/cli.rst: -------------------------------------------------------------------------------- 1 | .. _api-cli: 2 | 3 | vcs.cli 4 | ======= 5 | 6 | .. automodule:: vcs.cli 7 | 8 | 9 | .. command:: ExecutionManager 10 | 11 | vcs.cli.ExecutionManager 12 | ------------------------ 13 | 14 | .. autoclass:: vcs.cli.ExecutionManager 15 | :members: 16 | 17 | 18 | .. command:: BaseCommand 19 | 20 | vcs.cli.BaseCommand 21 | ------------------- 22 | 23 | .. autoclass:: vcs.cli.BaseCommand 24 | :members: 25 | 26 | 27 | 28 | .. command:: RepositoryCommand 29 | 30 | vcs.cli.RepositoryCommand 31 | ------------------------- 32 | 33 | .. autoclass:: vcs.cli.RepositoryCommand 34 | :members: 35 | 36 | 37 | 38 | .. command:: ChangesetCommand 39 | 40 | vcs.cli.ChangesetCommand 41 | ------------------------ 42 | 43 | .. autoclass:: vcs.cli.ChangesetCommand 44 | :members: 45 | 46 | 47 | 48 | .. command:: SingleChangesetCommand 49 | 50 | vcs.cli.SingleChangesetCommand 51 | ------------------------------ 52 | 53 | .. autoclass:: vcs.cli.SingleChangesetCommand 54 | :members: 55 | -------------------------------------------------------------------------------- /vcs/utils/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | abspath = lambda * p: os.path.abspath(os.path.join(*p)) 4 | 5 | 6 | def get_dirs_for_path(*paths): 7 | """ 8 | Returns list of directories, including intermediate. 9 | """ 10 | for path in paths: 11 | head = path 12 | while head: 13 | head, tail = os.path.split(head) 14 | if head: 15 | yield head 16 | else: 17 | # We don't need to yield empty path 18 | break 19 | 20 | 21 | def get_dir_size(path): 22 | root_path = path 23 | size = 0 24 | for path, dirs, files in os.walk(root_path): 25 | for f in files: 26 | try: 27 | size += os.path.getsize(os.path.join(path, f)) 28 | except OSError: 29 | pass 30 | return size 31 | 32 | 33 | def get_user_home(): 34 | """ 35 | Returns home path of the user. 36 | """ 37 | return os.getenv('HOME', os.getenv('USERPROFILE')) or '' 38 | -------------------------------------------------------------------------------- /vcs/commands/completion.py: -------------------------------------------------------------------------------- 1 | from vcs.cli import BaseCommand 2 | from vcs.cli import COMPLETION_ENV_NAME 3 | 4 | 5 | COMPLETION_TEMPLATE = ''' 6 | # %(prog_name)s bash completion start 7 | _%(prog_name)s_completion() 8 | { 9 | COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\ 10 | COMP_CWORD=$COMP_CWORD \\ 11 | %(ENV_VAR_NAME)s=1 $1 ) ) 12 | } 13 | complete -o default -F _%(prog_name)s_completion %(prog_name)s 14 | # %(prog_name)s bash completion end 15 | 16 | ''' 17 | 18 | 19 | class CompletionCommand(BaseCommand): 20 | help = ''.join(( 21 | 'Prints out shell snippet that once evaluated would allow ' 22 | 'this command utility to use completion abilities.', 23 | )) 24 | template = COMPLETION_TEMPLATE 25 | 26 | def get_completion_snippet(self): 27 | return self.template % {'prog_name': 'vcs', 28 | 'ENV_VAR_NAME': COMPLETION_ENV_NAME} 29 | 30 | def handle(self, **options): 31 | self.stdout.write(self.get_completion_snippet()) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010-2013 vcs Marcin Kuźmiński & Łukasz Balcerzak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /vcs/conf/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from vcs.utils import aslist 4 | from vcs.utils.paths import get_user_home 5 | 6 | abspath = lambda * p: os.path.abspath(os.path.join(*p)) 7 | 8 | VCSRC_PATH = os.environ.get('VCSRC_PATH') 9 | 10 | if not VCSRC_PATH: 11 | HOME_ = get_user_home() 12 | if not HOME_: 13 | HOME_ = tempfile.gettempdir() 14 | 15 | VCSRC_PATH = VCSRC_PATH or abspath(HOME_, '.vcsrc') 16 | if os.path.isdir(VCSRC_PATH): 17 | VCSRC_PATH = os.path.join(VCSRC_PATH, '__init__.py') 18 | 19 | # list of default encoding used in safe_unicode/safe_str methods 20 | DEFAULT_ENCODINGS = aslist('utf8') 21 | 22 | # path to git executable runned by run_git_command function 23 | GIT_EXECUTABLE_PATH = 'git' 24 | # can be also --branches --tags 25 | GIT_REV_FILTER = '--all' 26 | 27 | BACKENDS = { 28 | 'hg': 'vcs.backends.hg.MercurialRepository', 29 | 'git': 'vcs.backends.git.GitRepository', 30 | } 31 | 32 | ARCHIVE_SPECS = { 33 | 'tar': ('application/x-tar', '.tar'), 34 | 'tbz2': ('application/x-bzip2', '.tar.bz2'), 35 | 'tgz': ('application/x-gzip', '.tar.gz'), 36 | 'zip': ('application/zip', '.zip'), 37 | } 38 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | Welcome to vcs's documentation! 4 | =============================== 5 | 6 | ``vcs`` is abstraction layer over various version control systems. It is 7 | designed as feature-rich Python_ library with clear :ref:`API`. 8 | 9 | vcs uses `Semantic Versioning `_ 10 | 11 | **Features** 12 | 13 | - Common :ref:`API ` for SCM :ref:`backends ` 14 | - Fetching repositories data lazily 15 | - Simple caching mechanism so we don't hit repo too often 16 | - In memory commits API 17 | - Command Line Interface 18 | 19 | **Incoming** 20 | 21 | - Full working directories support 22 | - Extra backends: Subversion, Bazaar 23 | 24 | Documentation 25 | ============= 26 | 27 | **Installation:** 28 | 29 | .. toctree:: 30 | :maxdepth: 1 31 | 32 | quickstart 33 | installation 34 | usage/index 35 | contribute 36 | alternatives 37 | api/index 38 | license 39 | 40 | Other topics 41 | ============ 42 | 43 | * :ref:`genindex` 44 | * :ref:`search` 45 | 46 | .. _python: http://www.python.org/ 47 | .. _mercurial: http://mercurial.selenic.com/ 48 | .. _subversion: http://subversion.tigris.org/ 49 | .. _git: http://git-scm.com/ 50 | -------------------------------------------------------------------------------- /docs/api/conf.settings.rst: -------------------------------------------------------------------------------- 1 | .. _api-conf: 2 | 3 | vcs.conf.settings 4 | ================= 5 | 6 | .. automodule:: vcs.conf.settings 7 | 8 | 9 | .. setting:: ARCHIVE_SPECS 10 | 11 | ARCHIVE_SPECS 12 | ------------- 13 | 14 | Dictionary with mapping of *archive types* to *mimetypes*. 15 | 16 | Default:: 17 | 18 | { 19 | 'tar': ('application/x-tar', '.tar'), 20 | 'tbz2': ('application/x-bzip2', '.tar.bz2'), 21 | 'tgz': ('application/x-gzip', '.tar.gz'), 22 | 'zip': ('application/zip', '.zip'), 23 | } 24 | 25 | 26 | .. setting:: BACKENDS 27 | 28 | BACKENDS 29 | -------- 30 | 31 | Dictionary with mapping of *scm aliases* to *backend repository classes*. 32 | 33 | Default:: 34 | 35 | { 36 | 'hg': 'vcs.backends.hg.MercurialRepository', 37 | 'git': 'vcs.backends.git.GitRepository', 38 | } 39 | 40 | 41 | .. setting:: VCSRC_PATH 42 | 43 | VCSRC_PATH 44 | ---------- 45 | 46 | Points at a path where :command:`ExecutionManager` should look for module 47 | specified by user. By default it would be ``$HOME/.vimrc``. 48 | 49 | This value may be modified by setting system environment ``VCSRC_PATH`` 50 | (accessible at ``os.environ['VCSRC_PATH']``). 51 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | === 2 | VCS 3 | === 4 | 5 | .. image:: https://secure.travis-ci.org/codeinn/vcs.png?branch=master 6 | :target: http://travis-ci.org/codeinn/vcs 7 | 8 | various version control system management abstraction layer for python. 9 | 10 | ------------ 11 | Introduction 12 | ------------ 13 | 14 | ``vcs`` is abstraction layer over various version control systems. It is 15 | designed as feature-rich Python_ library with clean *API*. 16 | 17 | vcs uses `Semantic Versioning `_ 18 | 19 | **Features** 20 | 21 | - Common *API* for SCM backends 22 | - Fetching repositories data lazily 23 | - Simple caching mechanism so we don't hit repo too often 24 | - Simple commit api 25 | - Smart and powerfull in memory changesets 26 | - Working directory support 27 | 28 | 29 | ------------- 30 | Documentation 31 | ------------- 32 | 33 | Online documentation for development version is available at 34 | http://packages.python.org/vcs/. 35 | 36 | You may also build documentation for yourself - go into ``docs/`` and run:: 37 | 38 | make html 39 | 40 | .. _python: http://www.python.org/ 41 | .. _Sphinx: http://sphinx.pocoo.org/ 42 | .. _mercurial: http://mercurial.selenic.com/ 43 | .. _git: http://git-scm.com/ 44 | -------------------------------------------------------------------------------- /vcs/backends/git/workdir.py: -------------------------------------------------------------------------------- 1 | import re 2 | from vcs.backends.base import BaseWorkdir 3 | from vcs.exceptions import RepositoryError 4 | from vcs.exceptions import BranchDoesNotExistError 5 | 6 | 7 | class GitWorkdir(BaseWorkdir): 8 | 9 | def get_branch(self): 10 | headpath = self.repository._repo.refs.refpath('HEAD') 11 | try: 12 | content = open(headpath).read() 13 | match = re.match(r'^ref: refs/heads/(?P.+)\n$', content) 14 | if match: 15 | return match.groupdict()['branch'] 16 | else: 17 | raise RepositoryError("Couldn't compute workdir's branch") 18 | except IOError: 19 | # Try naive way... 20 | raise RepositoryError("Couldn't compute workdir's branch") 21 | 22 | def get_changeset(self): 23 | wk_dir_id = self.repository._repo.refs.as_dict().get('HEAD') 24 | return self.repository.get_changeset(wk_dir_id) 25 | 26 | def checkout_branch(self, branch=None): 27 | if branch is None: 28 | branch = self.repository.DEFAULT_BRANCH_NAME 29 | if branch not in self.repository.branches: 30 | raise BranchDoesNotExistError 31 | self.repository.run_git_command(['checkout', branch]) 32 | -------------------------------------------------------------------------------- /vcs/utils/archivers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class BaseArchiver(object): 5 | 6 | def __init__(self): 7 | self.archive_file = self._get_archive_file() 8 | 9 | def addfile(self): 10 | """ 11 | Adds a file to archive container 12 | """ 13 | pass 14 | 15 | def close(self): 16 | """ 17 | Closes and finalizes operation of archive container object 18 | """ 19 | self.archive_file.close() 20 | 21 | def _get_archive_file(self): 22 | """ 23 | Returns container for specific archive 24 | """ 25 | raise NotImplementedError() 26 | 27 | 28 | class TarArchiver(BaseArchiver): 29 | pass 30 | 31 | 32 | class Tbz2Archiver(BaseArchiver): 33 | pass 34 | 35 | 36 | class TgzArchiver(BaseArchiver): 37 | pass 38 | 39 | 40 | class ZipArchiver(BaseArchiver): 41 | pass 42 | 43 | 44 | def get_archiver(self, kind): 45 | """ 46 | Returns instance of archiver class specific to given kind 47 | 48 | :param kind: archive kind 49 | """ 50 | 51 | archivers = { 52 | 'tar': TarArchiver, 53 | 'tbz2': Tbz2Archiver, 54 | 'tgz': TgzArchiver, 55 | 'zip': ZipArchiver, 56 | } 57 | 58 | return archivers[kind]() 59 | -------------------------------------------------------------------------------- /vcs/utils/baseui_config.py: -------------------------------------------------------------------------------- 1 | from mercurial import ui, config 2 | 3 | 4 | def make_ui(self, path='hgwebdir.config'): 5 | """ 6 | A funcion that will read python rc files and make an ui from read options 7 | 8 | :param path: path to mercurial config file 9 | """ 10 | #propagated from mercurial documentation 11 | sections = [ 12 | 'alias', 13 | 'auth', 14 | 'decode/encode', 15 | 'defaults', 16 | 'diff', 17 | 'email', 18 | 'extensions', 19 | 'format', 20 | 'merge-patterns', 21 | 'merge-tools', 22 | 'hooks', 23 | 'http_proxy', 24 | 'smtp', 25 | 'patch', 26 | 'paths', 27 | 'profiling', 28 | 'server', 29 | 'trusted', 30 | 'ui', 31 | 'web', 32 | ] 33 | 34 | repos = path 35 | baseui = ui.ui() 36 | cfg = config.config() 37 | cfg.read(repos) 38 | self.paths = cfg.items('paths') 39 | self.base_path = self.paths[0][1].replace('*', '') 40 | self.check_repo_dir(self.paths) 41 | self.set_statics(cfg) 42 | 43 | for section in sections: 44 | for k, v in cfg.items(section): 45 | baseui.setconfig(section, k, v) 46 | 47 | return baseui 48 | -------------------------------------------------------------------------------- /vcs/tests/test_filenodes_unicode_path.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | 3 | from __future__ import with_statement 4 | 5 | import datetime 6 | from vcs.nodes import FileNode 7 | from vcs.utils.compat import unittest 8 | from vcs.tests.test_inmemchangesets import BackendBaseTestCase 9 | from vcs.tests.conf import SCM_TESTS 10 | 11 | 12 | class FileNodeUnicodePathTestsMixin(object): 13 | 14 | fname = 'ąśðąęłąć.txt' 15 | ufname = (fname).decode('utf-8') 16 | 17 | def get_commits(self): 18 | self.nodes = [ 19 | FileNode(self.fname, content='Foobar'), 20 | ] 21 | 22 | commits = [ 23 | { 24 | 'message': 'Initial commit', 25 | 'author': 'Joe Doe ', 26 | 'date': datetime.datetime(2010, 1, 1, 20), 27 | 'added': self.nodes, 28 | }, 29 | ] 30 | return commits 31 | 32 | def test_filenode_path(self): 33 | node = self.tip.get_node(self.fname) 34 | unode = self.tip.get_node(self.ufname) 35 | self.assertEqual(node, unode) 36 | 37 | 38 | for alias in SCM_TESTS: 39 | attrs = { 40 | 'backend_alias': alias, 41 | } 42 | cls_name = ''.join(('%s file node unicode path test' % alias).title() 43 | .split()) 44 | bases = (FileNodeUnicodePathTestsMixin, BackendBaseTestCase) 45 | globals()[cls_name] = type(cls_name, bases, attrs) 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /vcs/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class VCSError(Exception): 5 | pass 6 | 7 | 8 | class RepositoryError(VCSError): 9 | pass 10 | 11 | 12 | class EmptyRepositoryError(RepositoryError): 13 | pass 14 | 15 | 16 | class TagAlreadyExistError(RepositoryError): 17 | pass 18 | 19 | 20 | class TagDoesNotExistError(RepositoryError): 21 | pass 22 | 23 | 24 | class BranchAlreadyExistError(RepositoryError): 25 | pass 26 | 27 | 28 | class BranchDoesNotExistError(RepositoryError): 29 | pass 30 | 31 | 32 | class ChangesetError(RepositoryError): 33 | pass 34 | 35 | 36 | class ChangesetDoesNotExistError(ChangesetError): 37 | pass 38 | 39 | 40 | class CommitError(RepositoryError): 41 | pass 42 | 43 | 44 | class NothingChangedError(CommitError): 45 | pass 46 | 47 | 48 | class NodeError(VCSError): 49 | pass 50 | 51 | 52 | class RemovedFileNodeError(NodeError): 53 | pass 54 | 55 | 56 | class NodeAlreadyExistsError(CommitError): 57 | pass 58 | 59 | 60 | class NodeAlreadyChangedError(CommitError): 61 | pass 62 | 63 | 64 | class NodeDoesNotExistError(CommitError): 65 | pass 66 | 67 | 68 | class NodeNotChangedError(CommitError): 69 | pass 70 | 71 | 72 | class NodeAlreadyAddedError(CommitError): 73 | pass 74 | 75 | 76 | class NodeAlreadyRemovedError(CommitError): 77 | pass 78 | 79 | 80 | class ImproperArchiveTypeError(VCSError): 81 | pass 82 | 83 | 84 | class CommandError(VCSError): 85 | pass 86 | -------------------------------------------------------------------------------- /vcs/tests/test_getitem.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import datetime 4 | from vcs.tests.base import BackendTestMixin 5 | from vcs.tests.conf import SCM_TESTS 6 | from vcs.nodes import FileNode 7 | from vcs.utils.compat import unittest 8 | 9 | 10 | class GetitemTestCaseMixin(BackendTestMixin): 11 | 12 | @classmethod 13 | def _get_commits(cls): 14 | start_date = datetime.datetime(2010, 1, 1, 20) 15 | for x in xrange(5): 16 | yield { 17 | 'message': 'Commit %d' % x, 18 | 'author': 'Joe Doe ', 19 | 'date': start_date + datetime.timedelta(hours=12 * x), 20 | 'added': [ 21 | FileNode('file_%d.txt' % x, content='Foobar %d' % x), 22 | ], 23 | } 24 | 25 | def test__getitem__last_item_is_tip(self): 26 | self.assertEqual(self.repo[-1], self.repo.get_changeset()) 27 | 28 | def test__getitem__returns_correct_items(self): 29 | changesets = [self.repo[x] for x in xrange(len(self.repo.revisions))] 30 | self.assertEqual(changesets, list(self.repo.get_changesets())) 31 | 32 | 33 | # For each backend create test case class 34 | for alias in SCM_TESTS: 35 | attrs = { 36 | 'backend_alias': alias, 37 | } 38 | cls_name = ''.join(('%s getitem test' % alias).title().split()) 39 | bases = (GetitemTestCaseMixin, unittest.TestCase) 40 | globals()[cls_name] = type(cls_name, bases, attrs) 41 | 42 | 43 | if __name__ == '__main__': 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | vcs = __import__('vcs') 6 | readme_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 7 | 'README.rst')) 8 | 9 | try: 10 | long_description = open(readme_file).read() 11 | except IOError, err: 12 | sys.stderr.write("[ERROR] Cannot find file specified as " 13 | "long_description (%s)\n" % readme_file) 14 | sys.exit(1) 15 | 16 | install_requires = ['Pygments'] 17 | 18 | if sys.version_info < (2, 7): 19 | install_requires.append('unittest2') 20 | 21 | tests_require = install_requires + ['dulwich==0.10.0', 'mercurial==2.6.2', 'mock'] 22 | 23 | 24 | 25 | setup( 26 | name='vcs', 27 | version=vcs.get_version(), 28 | url='https://github.com/codeinn/vcs', 29 | author='Marcin Kuzminski, Lukasz Balcerzak', 30 | author_email='marcin@python-works.com', 31 | description=vcs.__doc__, 32 | long_description=long_description, 33 | zip_safe=False, 34 | packages=find_packages(), 35 | scripts=[], 36 | install_requires=install_requires, 37 | tests_require=tests_require, 38 | test_suite='vcs.tests.collector', 39 | include_package_data=True, 40 | classifiers=[ 41 | 'Development Status :: 4 - Beta', 42 | 'License :: OSI Approved :: MIT License', 43 | 'Intended Audience :: Developers', 44 | 'Programming Language :: Python', 45 | 'Operating System :: OS Independent', 46 | ], 47 | entry_points={ 48 | 'console_scripts': [ 49 | 'vcs = vcs:main', 50 | ], 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /docs/api/backends/git.rst: -------------------------------------------------------------------------------- 1 | .. _api-backends-git: 2 | 3 | vcs.backends.git 4 | ================ 5 | 6 | .. automodule:: vcs.backends.git 7 | 8 | GitRepository 9 | ------------- 10 | 11 | .. autoclass:: vcs.backends.git.GitRepository 12 | :members: 13 | 14 | GitChangeset 15 | ------------ 16 | 17 | .. autoclass:: vcs.backends.git.GitChangeset 18 | :members: 19 | :inherited-members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | .. autoattribute:: id 24 | 25 | Returns same as ``raw_id`` attribute. 26 | 27 | .. autoattribute:: raw_id 28 | 29 | Returns raw string identifing this changeset (40-length sha) 30 | 31 | .. autoattribute:: short_id 32 | 33 | Returns shortened version of ``raw_id`` (first 12 characters) 34 | 35 | .. autoattribute:: revision 36 | 37 | Returns integer representing changeset. 38 | 39 | .. autoattribute:: parents 40 | 41 | Returns list of parents changesets. 42 | 43 | .. autoattribute:: added 44 | 45 | Returns list of added ``FileNode`` objects. 46 | 47 | .. autoattribute:: changed 48 | 49 | Returns list of changed ``FileNode`` objects. 50 | 51 | .. autoattribute:: removed 52 | 53 | Returns list of removed ``RemovedFileNode`` objects. 54 | 55 | .. note:: 56 | Remember that those ``RemovedFileNode`` instances are only dummy 57 | ``FileNode`` objects and trying to access most of it's attributes or 58 | methods would raise ``NodeError`` exception. 59 | 60 | GitInMemoryChangeset 61 | -------------------- 62 | 63 | .. autoclass:: vcs.backends.git.GitInMemoryChangeset 64 | :members: 65 | :inherited-members: 66 | :undoc-members: 67 | :show-inheritance: 68 | -------------------------------------------------------------------------------- /docs/usage/vcsrc.rst: -------------------------------------------------------------------------------- 1 | .. _usage-vcsrc: 2 | 3 | vcsrc 4 | ===== 5 | 6 | During commands execution, vcs tries to build a module specified at 7 | :setting:`VCSRC_PATH`. This would fail silently if module does not exist 8 | - user is responsible for creating own *vcsrc* file. 9 | 10 | Creating own commands 11 | --------------------- 12 | 13 | User may create his own commands and add them dynamically by pointing them 14 | at ``vcs.cli.registry`` map. Here is very simple example of how *vcsrc* 15 | file could look like:: 16 | 17 | from vcs import cli 18 | 19 | class AuthorsCommand(cli.ChangesetCommand): 20 | 21 | def pre_process(self, repo): 22 | self.authors = {} 23 | 24 | def handle_changeset(self, changeset, **options): 25 | if changeset.author not in self.authors: 26 | self.authors[changeset.author] = 0 27 | self.authors[changeset.author] += 1 28 | 29 | def post_process(self, repo, **options): 30 | for author, changesets_number in self.authors.iteritems(): 31 | message = '%s : %s' % (author, changesets_number) 32 | self.stdout.write(message + '\n') 33 | 34 | cli.registry['authors'] = AuthorsCommand 35 | 36 | This would create ``AuthorsCommand`` that is mapped to ``authors`` subcommand. 37 | In order to run the command user would enter repository and type into terminal:: 38 | 39 | vcs authors 40 | 41 | As we have subclassed :command:`ChangesetCommand`, we also got all the 42 | changesets specified options. User may see whole help with following command:: 43 | 44 | vcs authors -h 45 | 46 | .. note:: 47 | Please refer to :ref:`api-cli` for more information about the basic commands. 48 | -------------------------------------------------------------------------------- /docs/api/backends/hg.rst: -------------------------------------------------------------------------------- 1 | .. _api-backends-hg: 2 | 3 | vcs.backends.hg 4 | =============== 5 | 6 | .. automodule:: vcs.backends.hg 7 | 8 | MercurialRepository 9 | ------------------- 10 | 11 | .. autoclass:: vcs.backends.hg.MercurialRepository 12 | :members: 13 | 14 | MercurialChangeset 15 | ------------------ 16 | 17 | .. autoclass:: vcs.backends.hg.MercurialChangeset 18 | :members: 19 | :inherited-members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | .. autoattribute:: id 24 | 25 | Returns shorter version of mercurial's changeset hexes. 26 | 27 | .. autoattribute:: raw_id 28 | 29 | Returns raw string identifying this changeset (40-length hex) 30 | 31 | .. autoattribute:: short_id 32 | 33 | Returns shortened version of ``raw_id`` (first 12 characters) 34 | 35 | .. autoattribute:: revision 36 | 37 | Returns integer representing changeset. 38 | 39 | .. autoattribute:: parents 40 | 41 | Returns list of parents changesets. 42 | 43 | .. autoattribute:: added 44 | 45 | Returns list of added ``FileNode`` objects. 46 | 47 | .. autoattribute:: changed 48 | 49 | Returns list of changed ``FileNode`` objects. 50 | 51 | .. autoattribute:: removed 52 | 53 | Returns list of removed ``RemovedFileNode`` objects. 54 | 55 | .. note:: 56 | Remember that those ``RemovedFileNode`` instances are only dummy 57 | ``FileNode`` objects and trying to access most of it's attributes or 58 | methods would raise ``NodeError`` exception. 59 | 60 | MercurialInMemoryChangeset 61 | -------------------------- 62 | 63 | .. autoclass:: MercurialInMemoryChangeset 64 | :members: 65 | :inherited-members: 66 | :undoc-members: 67 | :show-inheritance: 68 | -------------------------------------------------------------------------------- /vcs/utils/lazy.py: -------------------------------------------------------------------------------- 1 | class _Missing(object): 2 | 3 | def __repr__(self): 4 | return 'no value' 5 | 6 | def __reduce__(self): 7 | return '_missing' 8 | 9 | _missing = _Missing() 10 | 11 | 12 | class LazyProperty(object): 13 | """ 14 | Decorator for easier creation of ``property`` from potentially expensive to 15 | calculate attribute of the class. 16 | 17 | Usage:: 18 | 19 | class Foo(object): 20 | @LazyProperty 21 | def bar(self): 22 | print 'Calculating self._bar' 23 | return 42 24 | 25 | Taken from http://blog.pythonisito.com/2008/08/lazy-descriptors.html and 26 | used widely. 27 | """ 28 | 29 | def __init__(self, func): 30 | self._func = func 31 | self.__module__ = func.__module__ 32 | self.__name__ = func.__name__ 33 | self.__doc__ = func.__doc__ 34 | 35 | def __get__(self, obj, klass=None): 36 | if obj is None: 37 | return self 38 | value = obj.__dict__.get(self.__name__, _missing) 39 | if value is _missing: 40 | value = self._func(obj) 41 | obj.__dict__[self.__name__] = value 42 | return value 43 | 44 | import threading 45 | 46 | 47 | class ThreadLocalLazyProperty(LazyProperty): 48 | """ 49 | Same as above but uses thread local dict for cache storage. 50 | """ 51 | 52 | def __get__(self, obj, klass=None): 53 | if obj is None: 54 | return self 55 | if not hasattr(obj, '__tl_dict__'): 56 | obj.__tl_dict__ = threading.local().__dict__ 57 | 58 | value = obj.__tl_dict__.get(self.__name__, _missing) 59 | if value is _missing: 60 | value = self._func(obj) 61 | obj.__tl_dict__[self.__name__] = value 62 | return value 63 | -------------------------------------------------------------------------------- /vcs/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from pprint import pformat 4 | from vcs.conf import settings 5 | from vcs.exceptions import VCSError 6 | from vcs.utils.helpers import get_scm 7 | from vcs.utils.paths import abspath 8 | from vcs.utils.imports import import_class 9 | 10 | 11 | def get_repo(path=None, alias=None, create=False): 12 | """ 13 | Returns ``Repository`` object of type linked with given ``alias`` at 14 | the specified ``path``. If ``alias`` is not given it will try to guess it 15 | using get_scm method 16 | """ 17 | if create: 18 | if not (path or alias): 19 | raise TypeError("If create is specified, we need path and scm type") 20 | return get_backend(alias)(path, create=True) 21 | if path is None: 22 | path = abspath(os.path.curdir) 23 | try: 24 | scm, path = get_scm(path, search_up=True) 25 | path = abspath(path) 26 | alias = scm 27 | except VCSError: 28 | raise VCSError("No scm found at %s" % path) 29 | if alias is None: 30 | alias = get_scm(path)[0] 31 | 32 | backend = get_backend(alias) 33 | repo = backend(path, create=create) 34 | return repo 35 | 36 | 37 | def get_backend(alias): 38 | """ 39 | Returns ``Repository`` class identified by the given alias or raises 40 | VCSError if alias is not recognized or backend class cannot be imported. 41 | """ 42 | if alias not in settings.BACKENDS: 43 | raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n" 44 | "%s" % (alias, pformat(settings.BACKENDS.keys()))) 45 | backend_path = settings.BACKENDS[alias] 46 | klass = import_class(backend_path) 47 | return klass 48 | 49 | 50 | def get_supported_backends(): 51 | """ 52 | Returns list of aliases of supported backends. 53 | """ 54 | return settings.BACKENDS.keys() 55 | -------------------------------------------------------------------------------- /vcs/commands/log.py: -------------------------------------------------------------------------------- 1 | import string 2 | from vcs.nodes import FileNode 3 | from vcs.cli import ChangesetCommand 4 | from vcs.cli import make_option 5 | from vcs.utils.diffs import get_gitdiff 6 | 7 | 8 | class LogCommand(ChangesetCommand): 9 | TEMPLATE = u'$raw_id | $date | $message' 10 | 11 | option_list = ChangesetCommand.option_list + ( 12 | make_option('-t', '--template', action='store', dest='template', 13 | default=TEMPLATE, 14 | help='Specify own template. Default is: "%s"' % TEMPLATE, 15 | ), 16 | make_option('-p', '--patch', action='store_true', dest='show_patches', 17 | default=False, help='Show patches'), 18 | ) 19 | 20 | def get_last_commit(self, repo, cid=None): 21 | if cid is None: 22 | cid = repo.branches[repo.workdir.get_branch()] 23 | return repo.get_changeset(cid) 24 | 25 | def get_template(self, **options): 26 | return string.Template(options.get('template', self.TEMPLATE)) 27 | 28 | def handle_changeset(self, changeset, **options): 29 | template = self.get_template(**options) 30 | output = template.safe_substitute(**changeset.as_dict()) 31 | self.stdout.write(output) 32 | self.stdout.write('\n') 33 | 34 | if options.get('show_patches'): 35 | 36 | def show_diff(old_node, new_node): 37 | diff = get_gitdiff(old_node, new_node) 38 | self.stdout.write(u''.join(diff)) 39 | 40 | for node in changeset.added: 41 | show_diff(FileNode('null', content=''), node) 42 | for node in changeset.changed: 43 | old_node = node.history[0].get_node(node.path) 44 | show_diff(old_node, node) 45 | for node in changeset.removed: 46 | old_node = changeset.parents[0].get_node(node.path) 47 | new_node = FileNode(node.path, content='') 48 | show_diff(old_node, new_node) 49 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | .. _contribute: 2 | 3 | How to contribute 4 | ================= 5 | 6 | There are a lot of ways people may contribute to vcs. First of all, if you spot 7 | a bug please file it at `issue tracker `_. 8 | Moreover, if you feel you can fix the problem on your own and want to contribute 9 | to vcs, just fork from your preferred scm and send us your pull request. 10 | 11 | .. note:: 12 | Oh, some codes may be very ugly. If you spot ugly code, file a bug/clean it/ 13 | make it more readable/send us a note. 14 | 15 | 16 | Repositories 17 | ------------ 18 | 19 | As we do *various version control systems*, we also try to be flexible at where 20 | code resides and therefor, could be accessed by wider audience. 21 | 22 | - Main git repository is at https://github.com/codeinn/vcs/ 23 | - Main Mercurial repository is at https://bitbucket.org/marcinkuzminski/vcs/ 24 | 25 | We are going to create one *official* repository per supported 26 | :ref:`backend `. 27 | 28 | 29 | How to write backend 30 | -------------------- 31 | 32 | Don't see you favorite scm at supported :ref:`backends ` but like 33 | vcs :ref:`API `? Writing your own backend is in fact very simple process - 34 | all the backends should extend from :ref:`base backend `, 35 | however, as there are a few classes that needs to be written (repository, 36 | changeset, in-memory-changeset, workingdir) one would probably want to review 37 | existing backends' codebase. 38 | 39 | Tests 40 | ----- 41 | 42 | Tests are fundamental to vcs development process. In fact we try to do TDD_ as 43 | much as we can, however it doesn't always fit well with open source projects 44 | development. Nevertheless, we don't accept patches without tests. So... test, 45 | damn it! Whole heavy-lifting is done for you already, anyway (unless you don't 46 | intend to write new backend)! 47 | 48 | 49 | .. _TDD: http://en.wikipedia.org/wiki/Test-driven_development 50 | -------------------------------------------------------------------------------- /vcs/tests/test_getslice.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import datetime 4 | from vcs.tests.base import BackendTestMixin 5 | from vcs.tests.conf import SCM_TESTS 6 | from vcs.nodes import FileNode 7 | from vcs.utils.compat import unittest 8 | 9 | 10 | class GetsliceTestCaseMixin(BackendTestMixin): 11 | 12 | @classmethod 13 | def _get_commits(cls): 14 | start_date = datetime.datetime(2010, 1, 1, 20) 15 | for x in xrange(5): 16 | yield { 17 | 'message': 'Commit %d' % x, 18 | 'author': 'Joe Doe ', 19 | 'date': start_date + datetime.timedelta(hours=12 * x), 20 | 'added': [ 21 | FileNode('file_%d.txt' % x, content='Foobar %d' % x), 22 | ], 23 | } 24 | 25 | def test__getslice__last_item_is_tip(self): 26 | self.assertEqual(list(self.repo[-1:])[0], self.repo.get_changeset()) 27 | 28 | def test__getslice__respects_start_index(self): 29 | self.assertEqual(list(self.repo[2:]), 30 | [self.repo.get_changeset(rev) for rev in self.repo.revisions[2:]]) 31 | 32 | def test__getslice__respects_negative_start_index(self): 33 | self.assertEqual(list(self.repo[-2:]), 34 | [self.repo.get_changeset(rev) for rev in self.repo.revisions[-2:]]) 35 | 36 | def test__getslice__respects_end_index(self): 37 | self.assertEqual(list(self.repo[:2]), 38 | [self.repo.get_changeset(rev) for rev in self.repo.revisions[:2]]) 39 | 40 | def test__getslice__respects_negative_end_index(self): 41 | self.assertEqual(list(self.repo[:-2]), 42 | [self.repo.get_changeset(rev) for rev in self.repo.revisions[:-2]]) 43 | 44 | 45 | # For each backend create test case class 46 | for alias in SCM_TESTS: 47 | attrs = { 48 | 'backend_alias': alias, 49 | } 50 | cls_name = ''.join(('%s getslice test' % alias).title().split()) 51 | bases = (GetsliceTestCaseMixin, unittest.TestCase) 52 | globals()[cls_name] = type(cls_name, bases, attrs) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /vcs/tests/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests configuration module for vcs. 3 | """ 4 | import os 5 | import time 6 | import hashlib 7 | import tempfile 8 | import datetime 9 | import shutil 10 | from utils import get_normalized_path 11 | from os.path import join as jn 12 | 13 | __all__ = ( 14 | 'TEST_HG_REPO', 'TEST_GIT_REPO', 'HG_REMOTE_REPO', 'GIT_REMOTE_REPO', 15 | 'SCM_TESTS', 16 | ) 17 | 18 | SCM_TESTS = ['hg', 'git'] 19 | uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple()))) 20 | 21 | THIS = os.path.abspath(os.path.dirname(__file__)) 22 | 23 | GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git' 24 | 25 | TEST_TMP_PATH = os.environ.get('VCS_TEST_ROOT', '/tmp') 26 | TEST_GIT_REPO = os.environ.get('VCS_TEST_GIT_REPO', 27 | jn(TEST_TMP_PATH, 'vcs-git')) 28 | TEST_GIT_REPO_CLONE = os.environ.get('VCS_TEST_GIT_REPO_CLONE', 29 | jn(TEST_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)) 30 | TEST_GIT_REPO_PULL = os.environ.get('VCS_TEST_GIT_REPO_PULL', 31 | jn(TEST_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)) 32 | 33 | HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs' 34 | TEST_HG_REPO = os.environ.get('VCS_TEST_HG_REPO', 35 | jn(TEST_TMP_PATH, 'vcs-hg')) 36 | TEST_HG_REPO_CLONE = os.environ.get('VCS_TEST_HG_REPO_CLONE', 37 | jn(TEST_TMP_PATH, 'vcshgclone%s' % uniq_suffix)) 38 | TEST_HG_REPO_PULL = os.environ.get('VCS_TEST_HG_REPO_PULL', 39 | jn(TEST_TMP_PATH, 'vcshgpull%s' % uniq_suffix)) 40 | 41 | TEST_DIR = os.environ.get('VCS_TEST_ROOT', tempfile.gettempdir()) 42 | TEST_REPO_PREFIX = 'vcs-test' 43 | 44 | 45 | def get_new_dir(title): 46 | """ 47 | Returns always new directory path. 48 | """ 49 | name = TEST_REPO_PREFIX 50 | if title: 51 | name = '-'.join((name, title)) 52 | hex = hashlib.sha1(str(time.time())).hexdigest() 53 | name = '-'.join((name, hex)) 54 | path = os.path.join(TEST_DIR, name) 55 | return get_normalized_path(path) 56 | 57 | 58 | PACKAGE_DIR = os.path.abspath(os.path.join( 59 | os.path.dirname(__file__), '..')) 60 | _dest = jn(TEST_TMP_PATH, 'aconfig') 61 | shutil.copy(jn(THIS, 'aconfig'), _dest) 62 | TEST_USER_CONFIG_FILE = _dest 63 | -------------------------------------------------------------------------------- /vcs/tests/test_tags.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from vcs.tests.base import BackendTestMixin 4 | from vcs.tests.conf import SCM_TESTS 5 | from vcs.exceptions import TagAlreadyExistError 6 | from vcs.exceptions import TagDoesNotExistError 7 | from vcs.utils.compat import unittest 8 | 9 | 10 | class TagsTestCaseMixin(BackendTestMixin): 11 | 12 | def test_new_tag(self): 13 | tip = self.repo.get_changeset() 14 | tagsize = len(self.repo.tags) 15 | tag = self.repo.tag('last-commit', 'joe', tip.raw_id) 16 | 17 | self.assertEqual(len(self.repo.tags), tagsize + 1) 18 | for top, dirs, files in tip.walk(): 19 | self.assertEqual(top, tag.get_node(top.path)) 20 | 21 | def test_tag_already_exist(self): 22 | tip = self.repo.get_changeset() 23 | self.repo.tag('last-commit', 'joe', tip.raw_id) 24 | 25 | self.assertRaises(TagAlreadyExistError, 26 | self.repo.tag, 'last-commit', 'joe', tip.raw_id) 27 | 28 | chset = self.repo.get_changeset(0) 29 | self.assertRaises(TagAlreadyExistError, 30 | self.repo.tag, 'last-commit', 'jane', chset.raw_id) 31 | 32 | def test_remove_tag(self): 33 | tip = self.repo.get_changeset() 34 | self.repo.tag('last-commit', 'joe', tip.raw_id) 35 | tagsize = len(self.repo.tags) 36 | 37 | self.repo.remove_tag('last-commit', user='evil joe') 38 | self.assertEqual(len(self.repo.tags), tagsize - 1) 39 | 40 | def test_remove_tag_which_does_not_exist(self): 41 | self.assertRaises(TagDoesNotExistError, 42 | self.repo.remove_tag, 'last-commit', user='evil joe') 43 | 44 | def test_name_with_slash(self): 45 | self.repo.tag('19/10/11', 'joe') 46 | self.assertTrue('19/10/11' in self.repo.tags) 47 | self.repo.tag('11', 'joe') 48 | self.assertTrue('11' in self.repo.tags) 49 | 50 | # For each backend create test case class 51 | for alias in SCM_TESTS: 52 | attrs = { 53 | 'backend_alias': alias, 54 | } 55 | cls_name = ''.join(('%s tags test' % alias).title().split()) 56 | bases = (TagsTestCaseMixin, unittest.TestCase) 57 | globals()[cls_name] = type(cls_name, bases, attrs) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /vcs/tests/test_diffs.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from vcs.tests.base import BackendTestMixin 3 | from vcs.tests.conf import SCM_TESTS 4 | from vcs.nodes import FileNode 5 | from vcs.utils.compat import unittest 6 | from vcs.utils.diffs import get_gitdiff 7 | 8 | 9 | class DiffsTestMixin(BackendTestMixin): 10 | 11 | @classmethod 12 | def _get_commits(cls): 13 | commits = [ 14 | { 15 | 'message': u'Initial commit', 16 | 'author': u'Joe Doe ', 17 | 'date': datetime.datetime(2010, 1, 1, 20), 18 | 'added': [FileNode('file1', content='Foobar')], 19 | }, 20 | { 21 | 'message': u'Added a file2, change file1', 22 | 'author': u'Joe Doe ', 23 | 'date': datetime.datetime(2010, 1, 1, 20), 24 | 'added': [FileNode('file2', content='Foobar')], 25 | 'changed': [FileNode('file1', content='...')], 26 | }, 27 | { 28 | 'message': u'Remove file1', 29 | 'author': u'Joe Doe ', 30 | 'date': datetime.datetime(2010, 1, 1, 20), 31 | 'removed': [FileNode('file1')], 32 | }, 33 | ] 34 | return commits 35 | 36 | def test_log_command(self): 37 | commits = [self.repo.get_changeset(r) for r in self.repo.revisions] 38 | commit1, commit2, commit3 = commits 39 | 40 | old = commit1.get_node('file1') 41 | new = commit2.get_node('file1') 42 | result = get_gitdiff(old, new).splitlines() 43 | # there are small differences between git and hg output so we explicitly 44 | # check only few things 45 | self.assertEqual(result[0], 'diff --git a/file1 b/file1') 46 | self.assertIn('-Foobar', result) 47 | self.assertIn('+...', result) 48 | 49 | 50 | # For each backend create test case class 51 | for alias in SCM_TESTS: 52 | attrs = { 53 | 'backend_alias': alias, 54 | } 55 | cls_name = ''.join(('%s diff tests' % alias).title().split()) 56 | bases = (DiffsTestMixin, unittest.TestCase) 57 | globals()[cls_name] = type(cls_name, bases, attrs) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | 63 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | ``vcs`` is simply, pure python package. However, it makes use of various 7 | *version control systems* and thus, would require some third part libraries 8 | and they may have some deeper dependencies. 9 | 10 | Requirements 11 | ------------ 12 | 13 | Below is a table which shows requirements for each backend. 14 | 15 | +------------+---------------------+---------+---------------------+ 16 | | SCM | Backend | Alias | Requirements | 17 | +============+=====================+=========+=====================+ 18 | | Mercurial_ | ``vcs.backend.hg`` | ``hg`` | - mercurial_ >= 1.9 | 19 | +------------+---------------------+---------+---------------------+ 20 | | Git_ | ``vcs.backend.git`` | ``git`` | - git_ >= 1.7 | 21 | | | | | - Dulwich_ >= 0.8 | 22 | +------------+---------------------+---------+---------------------+ 23 | 24 | Install from Cheese Shop 25 | ------------------------ 26 | 27 | Easiest way to install ``vcs`` is to run:: 28 | 29 | easy_install vcs 30 | 31 | Or:: 32 | 33 | pip install vcs 34 | 35 | If you prefer to install manually simply grab latest release from 36 | http://pypi.python.org/pypi/vcs, decompress archive and run:: 37 | 38 | python setup.py install 39 | 40 | Development 41 | ----------- 42 | 43 | In order to test the package you'd need all backends underlying libraries (see 44 | table above) and unittest2_ as we use it to run test suites. 45 | 46 | Here is a full list of packages needed to run test suite: 47 | 48 | +-----------+---------------------------------------+ 49 | | Package | Homepage | 50 | +===========+=======================================+ 51 | | mock | http://pypi.python.org/pypi/mock | 52 | +-----------+---------------------------------------+ 53 | | unittest2 | http://pypi.python.org/pypi/unittest2 | 54 | +-----------+---------------------------------------+ 55 | | mercurial | http://mercurial.selenic.com/ | 56 | +-----------+---------------------------------------+ 57 | | git | http://git-scm.com | 58 | +-----------+---------------------------------------+ 59 | | dulwich | http://pypi.python.org/pypi/dulwich | 60 | +-----------+---------------------------------------+ 61 | 62 | .. _unittest2: http://pypi.python.org/pypi/unittest2 63 | .. _git: http://git-scm.com 64 | .. _dulwich: http://pypi.python.org/pypi/dulwich 65 | .. _mercurial: http://mercurial.selenic.com/ 66 | -------------------------------------------------------------------------------- /vcs/commands/summary.py: -------------------------------------------------------------------------------- 1 | from vcs.cli import make_option 2 | from vcs.cli import ChangesetCommand 3 | from vcs.utils.filesize import filesizeformat 4 | 5 | 6 | class SummaryCommand(ChangesetCommand): 7 | show_progress_bar = True 8 | 9 | option_list = ChangesetCommand.option_list + ( 10 | make_option('-s', '--with-changesets-size', action='store_true', 11 | dest='changeset_size', default=False, 12 | help='Counts size of filenodes from each commit [may be *heavy*]'), 13 | ) 14 | 15 | def __init__(self, *args, **kwargs): 16 | super(SummaryCommand, self).__init__(*args, **kwargs) 17 | self.total_size = 0 18 | self.authors = {} 19 | self.start_date = None 20 | self.last_date = None 21 | 22 | def handle_changeset(self, changeset, **options): 23 | if options['changeset_size']: 24 | self.total_size += changeset.size 25 | 26 | if changeset.author not in self.authors: 27 | self.authors[changeset.author] = { 28 | 'changeset_id_list': [changeset.raw_id], 29 | } 30 | else: 31 | self.authors[changeset.author]['changeset_id_list'].append( 32 | changeset.raw_id) 33 | 34 | if not self.start_date or changeset.date < self.start_date: 35 | self.start_date = changeset.date 36 | if not self.last_date or changeset.date > self.last_date: 37 | self.last_date = changeset.date 38 | 39 | 40 | 41 | def post_process(self, repo, **options): 42 | stats = [ 43 | ('Total repository size [HDD]', filesizeformat(repo.size)), 44 | ('Total number of commits', len(repo)), 45 | ('Total number of branches', len(repo.branches)), 46 | ('Total number of tags', len(repo.tags)), 47 | ('Total number of authors', len(self.authors)), 48 | ('Avarage number of commits/author', 49 | float(len(repo)) / len(self.authors)), 50 | ('Avarage number of commits/day', 51 | float(len(repo)) / (self.last_date - self.start_date).days), 52 | ] 53 | if options['changeset_size']: 54 | stats.append(('Total size in all changesets', 55 | filesizeformat(self.total_size))) 56 | 57 | max_label_size = max(len(label) for label, value in stats) 58 | output = [''] 59 | output.extend([ 60 | '%s: %s' % (label.rjust(max_label_size+3), value) 61 | for label, value in stats # pyflakes:ignore 62 | ]) 63 | output.append('') 64 | output.append('') 65 | self.stdout.write(u'\n'.join(output)) 66 | -------------------------------------------------------------------------------- /vcs/utils/lockfiles.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class LockFile(object): 5 | """Provides methods to obtain, check for, and release a file based lock which 6 | should be used to handle concurrent access to the same file. 7 | 8 | As we are a utility class to be derived from, we only use protected methods. 9 | 10 | Locks will automatically be released on destruction""" 11 | __slots__ = ("_file_path", "_owns_lock") 12 | 13 | def __init__(self, file_path): 14 | self._file_path = file_path 15 | self._owns_lock = False 16 | 17 | def __del__(self): 18 | self._release_lock() 19 | 20 | def _lock_file_path(self): 21 | """:return: Path to lockfile""" 22 | return "%s.lock" % (self._file_path) 23 | 24 | def _has_lock(self): 25 | """:return: True if we have a lock and if the lockfile still exists 26 | :raise AssertionError: if our lock-file does not exist""" 27 | if not self._owns_lock: 28 | return False 29 | 30 | return True 31 | 32 | def _obtain_lock_or_raise(self): 33 | """Create a lock file as flag for other instances, mark our instance as lock-holder 34 | 35 | :raise IOError: if a lock was already present or a lock file could not be written""" 36 | if self._has_lock(): 37 | return 38 | lock_file = self._lock_file_path() 39 | if os.path.isfile(lock_file): 40 | raise IOError("Lock for file %r did already exist, delete %r in case the lock is illegal" % (self._file_path, lock_file)) 41 | 42 | try: 43 | fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0) 44 | os.close(fd) 45 | except OSError,e: 46 | raise IOError(str(e)) 47 | 48 | self._owns_lock = True 49 | 50 | def _obtain_lock(self): 51 | """The default implementation will raise if a lock cannot be obtained. 52 | Subclasses may override this method to provide a different implementation""" 53 | return self._obtain_lock_or_raise() 54 | 55 | def _release_lock(self): 56 | """Release our lock if we have one""" 57 | if not self._has_lock(): 58 | return 59 | 60 | # if someone removed our file beforhand, lets just flag this issue 61 | # instead of failing, to make it more usable. 62 | lfp = self._lock_file_path() 63 | try: 64 | # on bloody windows, the file needs write permissions to be removable. 65 | # Why ... 66 | if os.name == 'nt': 67 | os.chmod(lfp, 0777) 68 | # END handle win32 69 | os.remove(lfp) 70 | except OSError: 71 | pass 72 | self._owns_lock = False 73 | -------------------------------------------------------------------------------- /vcs/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for vcs_ library. 3 | 4 | In order to run tests we need to prepare our environment first. Tests would be 5 | run for each engine listed at ``conf.SCM_TESTS`` - keys are aliases from 6 | ``vcs.backends.BACKENDS``. 7 | 8 | For each SCM we run tests for, we need some repository. We would use 9 | repositories location from system environment variables or test suite defaults 10 | - see ``conf`` module for more detail. We simply try to check if repository at 11 | certain location exists, if not we would try to fetch them. At ``test_vcs`` or 12 | ``test_common`` we run unit tests common for each repository type and for 13 | example specific mercurial tests are located at ``test_hg`` module. 14 | 15 | Oh, and tests are run with ``unittest.collector`` wrapped by ``collector`` 16 | function at ``tests/__init__.py``. 17 | 18 | .. _vcs: http://bitbucket.org/marcinkuzminski/vcs 19 | .. _unittest: http://pypi.python.org/pypi/unittest 20 | 21 | """ 22 | from vcs.utils.compat import unittest 23 | from vcs.tests.conf import * 24 | from vcs.tests.utils import VCSTestError, SCMFetcher 25 | 26 | # Import Test Cases 27 | from vcs.tests.base import * 28 | from test_branches import * 29 | from test_changesets import * 30 | from test_cli import * 31 | from test_diffs import * 32 | from test_filenodes_unicode_path import * 33 | from test_getitem import * 34 | from test_getslice import * 35 | from test_git import * 36 | from test_hg import * 37 | from test_inmemchangesets import * 38 | from test_nodes import * 39 | from test_repository import * 40 | from test_tags import * 41 | from test_utils import * 42 | from test_utils_filesize import * 43 | from test_utils_progressbar import * 44 | from test_vcs import * 45 | from test_workdirs import * 46 | 47 | 48 | def setup_package(): 49 | """ 50 | Prepares whole package for tests which mainly means it would try to fetch 51 | test repositories or use already existing ones. 52 | """ 53 | fetchers = { 54 | 'hg': { 55 | 'alias': 'hg', 56 | 'test_repo_path': TEST_HG_REPO, 57 | 'remote_repo': HG_REMOTE_REPO, 58 | 'clone_cmd': 'hg clone --insecure', 59 | }, 60 | 'git': { 61 | 'alias': 'git', 62 | 'test_repo_path': TEST_GIT_REPO, 63 | 'remote_repo': GIT_REMOTE_REPO, 64 | 'clone_cmd': 'git clone --bare', 65 | }, 66 | } 67 | try: 68 | for scm, fetcher_info in fetchers.items(): 69 | fetcher = SCMFetcher(**fetcher_info) 70 | fetcher.setup() 71 | except VCSTestError, err: 72 | raise RuntimeError(str(err)) 73 | 74 | 75 | def collector(): 76 | setup_package() 77 | start_dir = os.path.abspath(os.path.dirname(__file__)) 78 | return unittest.defaultTestLoader.discover(start_dir) 79 | 80 | 81 | def main(): 82 | collector() 83 | unittest.main() 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /vcs/tests/test_vcs.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import os 4 | import shutil 5 | 6 | from vcs import VCSError, get_repo, get_backend 7 | from vcs.backends.hg import MercurialRepository 8 | from vcs.utils.compat import unittest 9 | from vcs.tests.conf import TEST_HG_REPO, TEST_GIT_REPO, TEST_TMP_PATH 10 | 11 | 12 | 13 | class VCSTest(unittest.TestCase): 14 | """ 15 | Tests for main module's methods. 16 | """ 17 | 18 | def test_get_backend(self): 19 | hg = get_backend('hg') 20 | self.assertEqual(hg, MercurialRepository) 21 | 22 | def test_alias_detect_hg(self): 23 | alias = 'hg' 24 | path = TEST_HG_REPO 25 | backend = get_backend(alias) 26 | repo = backend(path) 27 | self.assertEqual('hg',repo.alias) 28 | 29 | def test_alias_detect_git(self): 30 | alias = 'git' 31 | path = TEST_GIT_REPO 32 | backend = get_backend(alias) 33 | repo = backend(path) 34 | self.assertEqual('git',repo.alias) 35 | 36 | def test_wrong_alias(self): 37 | alias = 'wrong_alias' 38 | self.assertRaises(VCSError, get_backend, alias) 39 | 40 | def test_get_repo(self): 41 | alias = 'hg' 42 | path = TEST_HG_REPO 43 | backend = get_backend(alias) 44 | repo = backend(path) 45 | 46 | self.assertEqual(repo.__class__, get_repo(path, alias).__class__) 47 | self.assertEqual(repo.path, get_repo(path, alias).path) 48 | 49 | def test_get_repo_autoalias_hg(self): 50 | alias = 'hg' 51 | path = TEST_HG_REPO 52 | backend = get_backend(alias) 53 | repo = backend(path) 54 | 55 | self.assertEqual(repo.__class__, get_repo(path).__class__) 56 | self.assertEqual(repo.path, get_repo(path).path) 57 | 58 | def test_get_repo_autoalias_git(self): 59 | alias = 'git' 60 | path = TEST_GIT_REPO 61 | backend = get_backend(alias) 62 | repo = backend(path) 63 | 64 | self.assertEqual(repo.__class__, get_repo(path).__class__) 65 | self.assertEqual(repo.path, get_repo(path).path) 66 | 67 | 68 | def test_get_repo_err(self): 69 | blank_repo_path = os.path.join(TEST_TMP_PATH, 'blank-error-repo') 70 | if os.path.isdir(blank_repo_path): 71 | shutil.rmtree(blank_repo_path) 72 | 73 | os.mkdir(blank_repo_path) 74 | self.assertRaises(VCSError, get_repo, blank_repo_path) 75 | self.assertRaises(VCSError, get_repo, blank_repo_path + 'non_existing') 76 | 77 | def test_get_repo_multialias(self): 78 | multialias_repo_path = os.path.join(TEST_TMP_PATH, 'hg-git-repo') 79 | if os.path.isdir(multialias_repo_path): 80 | shutil.rmtree(multialias_repo_path) 81 | 82 | os.mkdir(multialias_repo_path) 83 | 84 | os.mkdir(os.path.join(multialias_repo_path, '.git')) 85 | os.mkdir(os.path.join(multialias_repo_path, '.hg')) 86 | self.assertRaises(VCSError, get_repo, multialias_repo_path) 87 | -------------------------------------------------------------------------------- /vcs/commands/cat.py: -------------------------------------------------------------------------------- 1 | import os 2 | from vcs.cli import make_option 3 | from vcs.cli import SingleChangesetCommand 4 | 5 | 6 | class CatCommand(SingleChangesetCommand): 7 | """ 8 | Writes content of a target file to terminal. 9 | """ 10 | 11 | option_list = SingleChangesetCommand.option_list + ( 12 | make_option('--blame', action='store_true', dest='blame', 13 | default=False, 14 | help='Annotate output with '), 15 | make_option('--plain', action='store_true', dest='plain', 16 | default=False, 17 | help='Simply write output to terminal, don\'t use ' 18 | 'any extra formatting/colors (pygments).'), 19 | make_option('-n', '--line-numbers', action='store_true', dest='linenos', 20 | default=False, help='Shows line numbers'), 21 | ) 22 | 23 | def get_option_list(self): 24 | option_list = super(CatCommand, self).get_option_list() 25 | try: 26 | __import__('pygments') 27 | option = make_option('-f', '--formatter', action='store', 28 | dest='formatter_name', default='terminal', 29 | help='Pygments specific formatter name.', 30 | ) 31 | option_list += (option,) 32 | except ImportError: 33 | pass 34 | return option_list 35 | 36 | def get_text(self, node, **options): 37 | if options.get('plain'): 38 | return node.content 39 | formatter_name = options.get('formatter_name') 40 | if formatter_name: 41 | from pygments import highlight 42 | from pygments.formatters import get_formatter_by_name 43 | formatter = get_formatter_by_name(formatter_name) 44 | return highlight(node.content, node.lexer, formatter) 45 | return node.content 46 | 47 | def cat(self, node, **options): 48 | text = self.get_text(node, **options) 49 | 50 | if options.get('linenos'): 51 | lines = text.splitlines() 52 | linenos_width = len(str(len(lines))) 53 | text = '\n'.join(( 54 | '%s %s' % (str(lineno + 1).rjust(linenos_width), lines[lineno]) 55 | for lineno in xrange(len(lines)))) 56 | text += '\n' 57 | 58 | if options.get('blame'): 59 | lines = text.splitlines() 60 | output = [] 61 | author_width = 15 62 | for line in xrange(len(lines)): 63 | cs = node.annotate[line][1] 64 | output.append('%s |%s | %s' % ( 65 | cs.raw_id[:6], 66 | cs.author[:14].rjust(author_width), 67 | lines[line]) 68 | ) 69 | text = '\n'.join(output) 70 | text += '\n' 71 | 72 | self.stdout.write(text) 73 | 74 | 75 | def get_relative_filename(self, filename): 76 | return os.path.relpath(filename, self.repo.path) 77 | 78 | def handle_arg(self, changeset, arg, **options): 79 | filename = arg 80 | node = changeset.get_node(self.get_relative_filename(filename)) 81 | self.cat(node, **options) 82 | -------------------------------------------------------------------------------- /extras.py: -------------------------------------------------------------------------------- 1 | import _ast 2 | import os 3 | import sys 4 | from setuptools import Command 5 | 6 | 7 | def check(filename): 8 | from pyflakes import reporter as mod_reporter 9 | from pyflakes.checker import Checker 10 | codeString = open(filename).read() 11 | reporter = mod_reporter._makeDefaultReporter() 12 | # First, compile into an AST and handle syntax errors. 13 | try: 14 | tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) 15 | except SyntaxError: 16 | value = sys.exc_info()[1] 17 | msg = value.args[0] 18 | 19 | (lineno, offset, text) = value.lineno, value.offset, value.text 20 | 21 | # If there's an encoding problem with the file, the text is None. 22 | if text is None: 23 | # Avoid using msg, since for the only known case, it contains a 24 | # bogus message that claims the encoding the file declared was 25 | # unknown. 26 | reporter.unexpectedError(filename, 'problem decoding source') 27 | else: 28 | reporter.syntaxError(filename, msg, lineno, offset, text) 29 | return 1 30 | except Exception: 31 | reporter.unexpectedError(filename, 'problem decoding source') 32 | return 1 33 | else: 34 | # Okay, it's syntactically valid. Now check it. 35 | lines = codeString.splitlines() 36 | warnings = Checker(tree, filename) 37 | warnings.messages.sort(key=lambda m: m.lineno) 38 | real_messages = [] 39 | for m in warnings.messages: 40 | line = lines[m.lineno - 1] 41 | if 'pyflakes:ignore' in line.rsplit('#', 1)[-1]: 42 | # ignore lines with pyflakes:ignore 43 | pass 44 | else: 45 | real_messages.append(m) 46 | reporter.flake(m) 47 | return len(real_messages) 48 | 49 | 50 | class RunFlakesCommand(Command): 51 | """ 52 | Runs pyflakes against guardian codebase. 53 | """ 54 | description = "Check sources with pyflakes" 55 | user_options = [] 56 | ignore = ['__init__.py', '__main__.py', 'hgcompat.py'] 57 | 58 | def initialize_options(self): 59 | pass 60 | 61 | def finalize_options(self): 62 | pass 63 | 64 | def run(self): 65 | try: 66 | import pyflakes # pyflakes:ignore 67 | except ImportError: 68 | sys.stderr.write("No pyflakes installed!\n") 69 | sys.exit(-1) 70 | thisdir = os.path.dirname(__file__) 71 | vcsdir = os.path.join(thisdir, 'vcs') 72 | warns = 0 73 | # Define top-level directories 74 | for topdir, dirnames, filenames in os.walk(vcsdir): 75 | filenames = (f for f in filenames if f not in self.ignore) 76 | paths = (os.path.join(topdir, f) for f in filenames if f.endswith('.py')) 77 | for path in paths: 78 | if path.endswith('tests/__init__.py'): 79 | # ignore that module (it should only gather test cases with *) 80 | continue 81 | warns += check(path) 82 | if warns > 0: 83 | sys.stderr.write("ERROR: Finished with total %d warnings.\n" % warns) 84 | sys.exit(1) 85 | else: 86 | print("No problems found in source codes.") 87 | 88 | 89 | -------------------------------------------------------------------------------- /vcs/utils/ordered_dict.py: -------------------------------------------------------------------------------- 1 | """Ordered dict implementation""" 2 | from UserDict import DictMixin 3 | 4 | 5 | class OrderedDict(dict, DictMixin): 6 | 7 | def __init__(self, *args, **kwds): 8 | if len(args) > 1: 9 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 10 | try: 11 | self.__end 12 | except AttributeError: 13 | self.clear() 14 | self.update(*args, **kwds) 15 | 16 | def clear(self): 17 | self.__end = end = [] 18 | end += [None, end, end] # sentinel node for doubly linked list 19 | self.__map = {} # key --> [key, prev, next] 20 | dict.clear(self) 21 | 22 | def __setitem__(self, key, value): 23 | if key not in self: 24 | end = self.__end 25 | curr = end[1] 26 | curr[2] = end[1] = self.__map[key] = [key, curr, end] 27 | dict.__setitem__(self, key, value) 28 | 29 | def __delitem__(self, key): 30 | dict.__delitem__(self, key) 31 | key, prev, next = self.__map.pop(key) 32 | prev[2] = next 33 | next[1] = prev 34 | 35 | def __iter__(self): 36 | end = self.__end 37 | curr = end[2] 38 | while curr is not end: 39 | yield curr[0] 40 | curr = curr[2] 41 | 42 | def __reversed__(self): 43 | end = self.__end 44 | curr = end[1] 45 | while curr is not end: 46 | yield curr[0] 47 | curr = curr[1] 48 | 49 | def popitem(self, last=True): 50 | if not self: 51 | raise KeyError('dictionary is empty') 52 | if last: 53 | key = reversed(self).next() 54 | else: 55 | key = iter(self).next() 56 | value = self.pop(key) 57 | return key, value 58 | 59 | def __reduce__(self): 60 | items = [[k, self[k]] for k in self] 61 | tmp = self.__map, self.__end 62 | del self.__map, self.__end 63 | inst_dict = vars(self).copy() 64 | self.__map, self.__end = tmp 65 | if inst_dict: 66 | return (self.__class__, (items,), inst_dict) 67 | return self.__class__, (items,) 68 | 69 | def keys(self): 70 | return list(self) 71 | 72 | setdefault = DictMixin.setdefault 73 | update = DictMixin.update 74 | pop = DictMixin.pop 75 | values = DictMixin.values 76 | items = DictMixin.items 77 | iterkeys = DictMixin.iterkeys 78 | itervalues = DictMixin.itervalues 79 | iteritems = DictMixin.iteritems 80 | 81 | def __repr__(self): 82 | if not self: 83 | return '%s()' % (self.__class__.__name__,) 84 | return '%s(%r)' % (self.__class__.__name__, self.items()) 85 | 86 | def copy(self): 87 | return self.__class__(self) 88 | 89 | @classmethod 90 | def fromkeys(cls, iterable, value=None): 91 | d = cls() 92 | for key in iterable: 93 | d[key] = value 94 | return d 95 | 96 | def __eq__(self, other): 97 | if isinstance(other, OrderedDict): 98 | return len(self) == len(other) and self.items() == other.items() 99 | return dict.__eq__(self, other) 100 | 101 | def __ne__(self, other): 102 | return not self == other 103 | -------------------------------------------------------------------------------- /vcs/tests/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for tests only. These are not or should not be used normally - 3 | functions here are crafted as we don't want to use ``vcs`` to verify tests. 4 | """ 5 | import os 6 | import re 7 | import sys 8 | 9 | from subprocess import Popen 10 | 11 | 12 | class VCSTestError(Exception): 13 | pass 14 | 15 | 16 | def run_command(cmd, args): 17 | """ 18 | Runs command on the system with given ``args``. 19 | """ 20 | command = ' '.join((cmd, args)) 21 | p = Popen(command, shell=True) 22 | status = os.waitpid(p.pid, 0)[1] 23 | return status 24 | 25 | 26 | def eprint(msg): 27 | """ 28 | Prints given ``msg`` into sys.stderr as nose test runner hides all output 29 | from sys.stdout by default and if we want to pipe stream somewhere we don't 30 | need those verbose messages anyway. 31 | Appends line break. 32 | """ 33 | sys.stderr.write(msg) 34 | sys.stderr.write('\n') 35 | 36 | 37 | class SCMFetcher(object): 38 | 39 | def __init__(self, alias, test_repo_path, remote_repo, clone_cmd): 40 | """ 41 | :param clone_cmd: command which would clone remote repository; pass 42 | only first bits - remote path and destination would be appended 43 | using ``remote_repo`` and ``test_repo_path`` 44 | """ 45 | self.alias = alias 46 | self.test_repo_path = test_repo_path 47 | self.remote_repo = remote_repo 48 | self.clone_cmd = clone_cmd 49 | 50 | def setup(self): 51 | if not os.path.isdir(self.test_repo_path): 52 | self.fetch_repo() 53 | 54 | def fetch_repo(self): 55 | """ 56 | Tries to fetch repository from remote path. 57 | """ 58 | remote = self.remote_repo 59 | eprint("Fetching repository %s into %s" % (remote, self.test_repo_path)) 60 | run_command(self.clone_cmd, '%s %s' % (remote, self.test_repo_path)) 61 | 62 | 63 | def get_normalized_path(path): 64 | """ 65 | If given path exists, new path would be generated and returned. Otherwise 66 | same whats given is returned. Assumes that there would be no more than 67 | 10000 same named files. 68 | """ 69 | if os.path.exists(path): 70 | dir, basename = os.path.split(path) 71 | splitted_name = basename.split('.') 72 | if len(splitted_name) > 1: 73 | ext = splitted_name[-1] 74 | else: 75 | ext = None 76 | name = '.'.join(splitted_name[:-1]) 77 | matcher = re.compile(r'^.*-(\d{5})$') 78 | start = 0 79 | m = matcher.match(name) 80 | if not m: 81 | # Haven't append number yet so return first 82 | newname = '%s-00000' % name 83 | newpath = os.path.join(dir, newname) 84 | if ext: 85 | newpath = '.'.join((newpath, ext)) 86 | return get_normalized_path(newpath) 87 | else: 88 | start = int(m.group(1)[-5:]) + 1 89 | for x in xrange(start, 10000): 90 | newname = name[:-5] + str(x).rjust(5, '0') 91 | newpath = os.path.join(dir, newname) 92 | if ext: 93 | newpath = '.'.join((newpath, ext)) 94 | if not os.path.exists(newpath): 95 | return newpath 96 | raise VCSTestError("Couldn't compute new path for %s" % path) 97 | return path 98 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-projector.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-projector.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | 91 | pdf: 92 | $(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf 93 | @echo 94 | @echo "Build finished. The PDF files are in $(BUILDDIR)/pdf." 95 | 96 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. dirhtml to make HTML files named index.html in directories 19 | echo. pickle to make pickle files 20 | echo. json to make JSON files 21 | echo. htmlhelp to make HTML files and a HTML help project 22 | echo. qthelp to make HTML files and a qthelp project 23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 24 | echo. changes to make an overview over all changed/added/deprecated items 25 | echo. linkcheck to check all external links for integrity 26 | echo. doctest to run all doctests embedded in the documentation if enabled 27 | goto end 28 | ) 29 | 30 | if "%1" == "clean" ( 31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 32 | del /q /s %BUILDDIR%\* 33 | goto end 34 | ) 35 | 36 | if "%1" == "html" ( 37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 38 | echo. 39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 40 | goto end 41 | ) 42 | 43 | if "%1" == "dirhtml" ( 44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 47 | goto end 48 | ) 49 | 50 | if "%1" == "pickle" ( 51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 52 | echo. 53 | echo.Build finished; now you can process the pickle files. 54 | goto end 55 | ) 56 | 57 | if "%1" == "json" ( 58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 59 | echo. 60 | echo.Build finished; now you can process the JSON files. 61 | goto end 62 | ) 63 | 64 | if "%1" == "htmlhelp" ( 65 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 66 | echo. 67 | echo.Build finished; now you can run HTML Help Workshop with the ^ 68 | .hhp project file in %BUILDDIR%/htmlhelp. 69 | goto end 70 | ) 71 | 72 | if "%1" == "qthelp" ( 73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 74 | echo. 75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 76 | .qhcp project file in %BUILDDIR%/qthelp, like this: 77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-projector.qhcp 78 | echo.To view the help file: 79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-projector.ghc 80 | goto end 81 | ) 82 | 83 | if "%1" == "latex" ( 84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 85 | echo. 86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 87 | goto end 88 | ) 89 | 90 | if "%1" == "changes" ( 91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 92 | echo. 93 | echo.The overview file is in %BUILDDIR%/changes. 94 | goto end 95 | ) 96 | 97 | if "%1" == "linkcheck" ( 98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 99 | echo. 100 | echo.Link check complete; look for any errors in the above output ^ 101 | or in %BUILDDIR%/linkcheck/output.txt. 102 | goto end 103 | ) 104 | 105 | if "%1" == "doctest" ( 106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 107 | echo. 108 | echo.Testing of doctests in the sources finished, look at the ^ 109 | results in %BUILDDIR%/doctest/output.txt. 110 | goto end 111 | ) 112 | 113 | :end 114 | -------------------------------------------------------------------------------- /vcs/tests/test_workdirs.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import datetime 4 | from vcs.nodes import FileNode 5 | from vcs.utils.compat import unittest 6 | from vcs.tests.base import BackendTestMixin 7 | from vcs.tests.conf import SCM_TESTS 8 | 9 | 10 | class WorkdirTestCaseMixin(BackendTestMixin): 11 | 12 | @classmethod 13 | def _get_commits(cls): 14 | commits = [ 15 | { 16 | 'message': u'Initial commit', 17 | 'author': u'Joe Doe ', 18 | 'date': datetime.datetime(2010, 1, 1, 20), 19 | 'added': [ 20 | FileNode('foobar', content='Foobar'), 21 | FileNode('foobar2', content='Foobar II'), 22 | FileNode('foo/bar/baz', content='baz here!'), 23 | ], 24 | }, 25 | { 26 | 'message': u'Changes...', 27 | 'author': u'Jane Doe ', 28 | 'date': datetime.datetime(2010, 1, 1, 21), 29 | 'added': [ 30 | FileNode('some/new.txt', content='news...'), 31 | ], 32 | 'changed': [ 33 | FileNode('foobar', 'Foobar I'), 34 | ], 35 | 'removed': [], 36 | }, 37 | ] 38 | return commits 39 | 40 | def test_get_branch_for_default_branch(self): 41 | self.assertEqual(self.repo.workdir.get_branch(), 42 | self.repo.DEFAULT_BRANCH_NAME) 43 | 44 | def test_get_branch_after_adding_one(self): 45 | self.imc.add(FileNode('docs/index.txt', 46 | content='Documentation\n')) 47 | self.imc.commit( 48 | message=u'New branch: foobar', 49 | author=u'joe', 50 | branch='foobar', 51 | ) 52 | self.assertEqual(self.repo.workdir.get_branch(), self.default_branch) 53 | 54 | def test_get_changeset(self): 55 | old_head = self.repo.get_changeset() 56 | self.imc.add(FileNode('docs/index.txt', 57 | content='Documentation\n')) 58 | head = self.imc.commit( 59 | message=u'New branch: foobar', 60 | author=u'joe', 61 | branch='foobar', 62 | ) 63 | self.assertEqual(self.repo.workdir.get_branch(), self.default_branch) 64 | self.repo.workdir.checkout_branch('foobar') 65 | self.assertEqual(self.repo.workdir.get_changeset(), head) 66 | 67 | # Make sure that old head is still there after update to defualt branch 68 | self.repo.workdir.checkout_branch(self.default_branch) 69 | self.assertEqual(self.repo.workdir.get_changeset(), old_head) 70 | 71 | def test_checkout_branch(self): 72 | from vcs.exceptions import BranchDoesNotExistError 73 | # first, 'foobranch' does not exist. 74 | self.assertRaises(BranchDoesNotExistError, self.repo.workdir.checkout_branch, 75 | branch='foobranch') 76 | # create new branch 'foobranch'. 77 | self.imc.add(FileNode('file1', content='blah')) 78 | self.imc.commit(message=u'asd', author=u'john', branch='foobranch') 79 | # go back to the default branch 80 | self.repo.workdir.checkout_branch() 81 | self.assertEqual(self.repo.workdir.get_branch(), self.backend_class.DEFAULT_BRANCH_NAME) 82 | # checkout 'foobranch' 83 | self.repo.workdir.checkout_branch('foobranch') 84 | self.assertEqual(self.repo.workdir.get_branch(), 'foobranch') 85 | 86 | 87 | # For each backend create test case class 88 | for alias in SCM_TESTS: 89 | attrs = { 90 | 'backend_alias': alias, 91 | } 92 | cls_name = ''.join(('%s branch test' % alias).title().split()) 93 | bases = (WorkdirTestCaseMixin, unittest.TestCase) 94 | globals()[cls_name] = type(cls_name, bases, attrs) 95 | 96 | 97 | if __name__ == '__main__': 98 | unittest.main() 99 | -------------------------------------------------------------------------------- /vcs/tests/test_archives.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import os 4 | import tarfile 5 | import zipfile 6 | import datetime 7 | import tempfile 8 | import StringIO 9 | from vcs.tests.base import BackendTestMixin 10 | from vcs.tests.conf import SCM_TESTS 11 | from vcs.exceptions import VCSError 12 | from vcs.nodes import FileNode 13 | from vcs.utils.compat import unittest 14 | 15 | 16 | class ArchivesTestCaseMixin(BackendTestMixin): 17 | 18 | @classmethod 19 | def _get_commits(cls): 20 | start_date = datetime.datetime(2010, 1, 1, 20) 21 | for x in xrange(5): 22 | yield { 23 | 'message': 'Commit %d' % x, 24 | 'author': 'Joe Doe ', 25 | 'date': start_date + datetime.timedelta(hours=12 * x), 26 | 'added': [ 27 | FileNode('%d/file_%d.txt' % (x, x), 28 | content='Foobar %d' % x), 29 | ], 30 | } 31 | 32 | def test_archive_zip(self): 33 | path = tempfile.mkstemp()[1] 34 | with open(path, 'wb') as f: 35 | self.tip.fill_archive(stream=f, kind='zip', prefix='repo') 36 | out = zipfile.ZipFile(path) 37 | 38 | for x in xrange(5): 39 | node_path = '%d/file_%d.txt' % (x, x) 40 | decompressed = StringIO.StringIO() 41 | decompressed.write(out.read('repo/' + node_path)) 42 | self.assertEqual( 43 | decompressed.getvalue(), 44 | self.tip.get_node(node_path).content) 45 | 46 | def test_archive_tgz(self): 47 | path = tempfile.mkstemp()[1] 48 | with open(path, 'wb') as f: 49 | self.tip.fill_archive(stream=f, kind='tgz', prefix='repo') 50 | outdir = tempfile.mkdtemp() 51 | 52 | outfile = tarfile.open(path, 'r|gz') 53 | outfile.extractall(outdir) 54 | 55 | for x in xrange(5): 56 | node_path = '%d/file_%d.txt' % (x, x) 57 | self.assertEqual( 58 | open(os.path.join(outdir, 'repo/' + node_path)).read(), 59 | self.tip.get_node(node_path).content) 60 | 61 | def test_archive_tbz2(self): 62 | path = tempfile.mkstemp()[1] 63 | with open(path, 'w+b') as f: 64 | self.tip.fill_archive(stream=f, kind='tbz2', prefix='repo') 65 | outdir = tempfile.mkdtemp() 66 | 67 | outfile = tarfile.open(path, 'r|bz2') 68 | outfile.extractall(outdir) 69 | 70 | for x in xrange(5): 71 | node_path = '%d/file_%d.txt' % (x, x) 72 | self.assertEqual( 73 | open(os.path.join(outdir, 'repo/' + node_path)).read(), 74 | self.tip.get_node(node_path).content) 75 | 76 | def test_archive_default_stream(self): 77 | tmppath = tempfile.mkstemp()[1] 78 | with open(tmppath, 'w') as stream: 79 | self.tip.fill_archive(stream=stream) 80 | mystream = StringIO.StringIO() 81 | self.tip.fill_archive(stream=mystream) 82 | mystream.seek(0) 83 | with open(tmppath, 'r') as f: 84 | self.assertEqual(f.read(), mystream.read()) 85 | 86 | def test_archive_wrong_kind(self): 87 | with self.assertRaises(VCSError): 88 | self.tip.fill_archive(kind='wrong kind') 89 | 90 | def test_archive_empty_prefix(self): 91 | with self.assertRaises(VCSError): 92 | self.tip.fill_archive(prefix='') 93 | 94 | def test_archive_prefix_with_leading_slash(self): 95 | with self.assertRaises(VCSError): 96 | self.tip.fill_archive(prefix='/any') 97 | 98 | # For each backend create test case class 99 | for alias in SCM_TESTS: 100 | attrs = { 101 | 'backend_alias': alias, 102 | } 103 | cls_name = ''.join(('%s archive test' % alias).title().split()) 104 | bases = (ArchivesTestCaseMixin, unittest.TestCase) 105 | globals()[cls_name] = type(cls_name, bases, attrs) 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /vcs/tests/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module providing backend independent mixin class. It requires that 3 | InMemoryChangeset class is working properly at backend class. 4 | """ 5 | import logging 6 | import os 7 | import time 8 | import shutil 9 | import datetime 10 | from vcs.tests.conf import SCM_TESTS, get_new_dir 11 | 12 | import vcs 13 | from vcs.utils.compat import unittest 14 | from vcs.nodes import FileNode 15 | 16 | 17 | class BackendTestMixin(object): 18 | """ 19 | This is a backend independent test case class which should be created 20 | with ``type`` method. 21 | 22 | It is required to set following attributes at subclass: 23 | 24 | - ``backend_alias``: alias of used backend (see ``vcs.BACKENDS``) 25 | - ``repo_path``: path to the repository which would be created for set of 26 | tests 27 | - ``recreate_repo_per_test``: If set to ``False``, repo would NOT be created 28 | before every single test. Defaults to ``True``. 29 | """ 30 | recreate_repo_per_test = True 31 | 32 | @classmethod 33 | def get_backend(cls): 34 | return vcs.get_backend(cls.backend_alias) 35 | 36 | @classmethod 37 | def _get_commits(cls): 38 | commits = [ 39 | { 40 | 'message': u'Initial commit', 41 | 'author': u'Joe Doe ', 42 | 'date': datetime.datetime(2010, 1, 1, 20), 43 | 'added': [ 44 | FileNode('foobar', content='Foobar'), 45 | FileNode('foobar2', content='Foobar II'), 46 | FileNode('foo/bar/baz', content='baz here!'), 47 | ], 48 | }, 49 | { 50 | 'message': u'Changes...', 51 | 'author': u'Jane Doe ', 52 | 'date': datetime.datetime(2010, 1, 1, 21), 53 | 'added': [ 54 | FileNode('some/new.txt', content='news...'), 55 | ], 56 | 'changed': [ 57 | FileNode('foobar', 'Foobar I'), 58 | ], 59 | 'removed': [], 60 | }, 61 | ] 62 | return commits 63 | 64 | @classmethod 65 | def setUpClass(cls): 66 | logging.basicConfig(level=logging.INFO) 67 | Backend = cls.get_backend() 68 | cls.backend_class = Backend 69 | cls.repo_path = get_new_dir(str(time.time())) 70 | cls.repo = Backend(cls.repo_path, create=True) 71 | cls.imc = cls.repo.in_memory_changeset 72 | cls.default_branch = cls.repo.DEFAULT_BRANCH_NAME 73 | 74 | for commit in cls._get_commits(): 75 | for node in commit.get('added', []): 76 | cls.imc.add(FileNode(node.path, content=node.content)) 77 | for node in commit.get('changed', []): 78 | cls.imc.change(FileNode(node.path, content=node.content)) 79 | for node in commit.get('removed', []): 80 | cls.imc.remove(FileNode(node.path)) 81 | 82 | cls.tip = cls.imc.commit(message=unicode(commit['message']), 83 | author=unicode(commit['author']), 84 | date=commit['date']) 85 | 86 | @classmethod 87 | def tearDownClass(cls): 88 | if not getattr(cls, 'recreate_repo_per_test', False) and \ 89 | 'VCS_REMOVE_TEST_DIRS' in os.environ: 90 | shutil.rmtree(cls.repo_path) 91 | 92 | def setUp(self): 93 | if getattr(self, 'recreate_repo_per_test', False): 94 | self.__class__.setUpClass() 95 | 96 | def tearDown(self): 97 | if getattr(self, 'recreate_repo_per_test', False) and \ 98 | 'VCS_REMOVE_TEST_DIRS' in os.environ: 99 | shutil.rmtree(self.repo_path) 100 | 101 | 102 | # For each backend create test case class 103 | for alias in SCM_TESTS: 104 | attrs = { 105 | 'backend_alias': alias, 106 | } 107 | cls_name = ''.join(('%s base backend test' % alias).title().split()) 108 | bases = (BackendTestMixin, unittest.TestCase) 109 | globals()[cls_name] = type(cls_name, bases, attrs) 110 | 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /vcs/tests/test_branches.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import datetime 4 | import vcs 5 | from vcs.utils.compat import unittest 6 | from vcs.nodes import FileNode 7 | 8 | from vcs.tests.base import BackendTestMixin 9 | from vcs.tests.conf import SCM_TESTS 10 | 11 | 12 | class BranchesTestCaseMixin(BackendTestMixin): 13 | 14 | @classmethod 15 | def _get_commits(cls): 16 | commits = [ 17 | { 18 | 'message': 'Initial commit', 19 | 'author': 'Joe Doe ', 20 | 'date': datetime.datetime(2010, 1, 1, 20), 21 | 'added': [ 22 | FileNode('foobar', content='Foobar'), 23 | FileNode('foobar2', content='Foobar II'), 24 | FileNode('foo/bar/baz', content='baz here!'), 25 | ], 26 | }, 27 | { 28 | 'message': 'Changes...', 29 | 'author': 'Jane Doe ', 30 | 'date': datetime.datetime(2010, 1, 1, 21), 31 | 'added': [ 32 | FileNode('some/new.txt', content='news...'), 33 | ], 34 | 'changed': [ 35 | FileNode('foobar', 'Foobar I'), 36 | ], 37 | 'removed': [], 38 | }, 39 | ] 40 | return commits 41 | 42 | def test_simple(self): 43 | tip = self.repo.get_changeset() 44 | self.assertEqual(tip.date, datetime.datetime(2010, 1, 1, 21)) 45 | 46 | def test_new_branch(self): 47 | # This check must not be removed to ensure the 'branches' LazyProperty 48 | # gets hit *before* the new 'foobar' branch got created: 49 | self.assertFalse('foobar' in self.repo.branches) 50 | self.imc.add(vcs.nodes.FileNode('docs/index.txt', 51 | content='Documentation\n')) 52 | foobar_tip = self.imc.commit( 53 | message=u'New branch: foobar', 54 | author=u'joe', 55 | branch='foobar', 56 | ) 57 | self.assertTrue('foobar' in self.repo.branches) 58 | self.assertEqual(foobar_tip.branch, 'foobar') 59 | 60 | def test_new_head(self): 61 | tip = self.repo.get_changeset() 62 | self.imc.add(vcs.nodes.FileNode('docs/index.txt', 63 | content='Documentation\n')) 64 | foobar_tip = self.imc.commit( 65 | message=u'New branch: foobar', 66 | author=u'joe', 67 | branch='foobar', 68 | parents=[tip], 69 | ) 70 | self.imc.change(vcs.nodes.FileNode('docs/index.txt', 71 | content='Documentation\nand more...\n')) 72 | newtip = self.imc.commit( 73 | message=u'At default branch', 74 | author=u'joe', 75 | branch=foobar_tip.branch, 76 | parents=[foobar_tip], 77 | ) 78 | 79 | newest_tip = self.imc.commit( 80 | message=u'Merged with %s' % foobar_tip.raw_id, 81 | author=u'joe', 82 | branch=self.backend_class.DEFAULT_BRANCH_NAME, 83 | parents=[newtip, foobar_tip], 84 | ) 85 | 86 | self.assertEqual(newest_tip.branch, 87 | self.backend_class.DEFAULT_BRANCH_NAME) 88 | 89 | def test_branch_with_slash_in_name(self): 90 | self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n')) 91 | self.imc.commit(u'Branch with a slash!', author=u'joe', 92 | branch='issue/123') 93 | self.assertTrue('issue/123' in self.repo.branches) 94 | 95 | def test_branch_with_slash_in_name_and_similar_without(self): 96 | self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n')) 97 | self.imc.commit(u'Branch with a slash!', author=u'joe', 98 | branch='issue/123') 99 | self.imc.add(vcs.nodes.FileNode('extrafile II', content='Some data\n')) 100 | self.imc.commit(u'Branch without a slash...', author=u'joe', 101 | branch='123') 102 | self.assertIn('issue/123', self.repo.branches) 103 | self.assertIn('123', self.repo.branches) 104 | 105 | 106 | # For each backend create test case class 107 | for alias in SCM_TESTS: 108 | attrs = { 109 | 'backend_alias': alias, 110 | } 111 | cls_name = ''.join(('%s branches test' % alias).title().split()) 112 | bases = (BranchesTestCaseMixin, unittest.TestCase) 113 | globals()[cls_name] = type(cls_name, bases, attrs) 114 | 115 | 116 | if __name__ == '__main__': 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /vcs/backends/hg/inmemory.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import errno 3 | 4 | from vcs.backends.base import BaseInMemoryChangeset 5 | from vcs.exceptions import RepositoryError 6 | 7 | from vcs.utils.hgcompat import memfilectx, memctx, hex, tolocal 8 | 9 | 10 | class MercurialInMemoryChangeset(BaseInMemoryChangeset): 11 | 12 | def commit(self, message, author, parents=None, branch=None, date=None, 13 | **kwargs): 14 | """ 15 | Performs in-memory commit (doesn't check workdir in any way) and 16 | returns newly created ``Changeset``. Updates repository's 17 | ``revisions``. 18 | 19 | :param message: message of the commit 20 | :param author: full username, i.e. "Joe Doe " 21 | :param parents: single parent or sequence of parents from which commit 22 | would be derieved 23 | :param date: ``datetime.datetime`` instance. Defaults to 24 | ``datetime.datetime.now()``. 25 | :param branch: branch name, as string. If none given, default backend's 26 | branch would be used. 27 | 28 | :raises ``CommitError``: if any error occurs while committing 29 | """ 30 | self.check_integrity(parents) 31 | 32 | from .repository import MercurialRepository 33 | if not isinstance(message, unicode) or not isinstance(author, unicode): 34 | raise RepositoryError('Given message and author needs to be ' 35 | 'an instance got %r & %r instead' 36 | % (type(message), type(author))) 37 | 38 | if branch is None: 39 | branch = MercurialRepository.DEFAULT_BRANCH_NAME 40 | kwargs['branch'] = branch 41 | 42 | def filectxfn(_repo, memctx, path): 43 | """ 44 | Marks given path as added/changed/removed in a given _repo. This is 45 | for internal mercurial commit function. 46 | """ 47 | 48 | # check if this path is removed 49 | if path in (node.path for node in self.removed): 50 | # Raising exception is a way to mark node for removal 51 | raise IOError(errno.ENOENT, '%s is deleted' % path) 52 | 53 | # check if this path is added 54 | for node in self.added: 55 | if node.path == path: 56 | return memfilectx(path=node.path, 57 | data=(node.content.encode('utf8') 58 | if not node.is_binary else node.content), 59 | islink=False, 60 | isexec=node.is_executable, 61 | copied=False) 62 | 63 | # or changed 64 | for node in self.changed: 65 | if node.path == path: 66 | return memfilectx(path=node.path, 67 | data=(node.content.encode('utf8') 68 | if not node.is_binary else node.content), 69 | islink=False, 70 | isexec=node.is_executable, 71 | copied=False) 72 | 73 | raise RepositoryError("Given path haven't been marked as added," 74 | "changed or removed (%s)" % path) 75 | 76 | parents = [None, None] 77 | for i, parent in enumerate(self.parents): 78 | if parent is not None: 79 | parents[i] = parent._ctx.node() 80 | 81 | if date and isinstance(date, datetime.datetime): 82 | date = date.ctime() 83 | 84 | commit_ctx = memctx(repo=self.repository._repo, 85 | parents=parents, 86 | text='', 87 | files=self.get_paths(), 88 | filectxfn=filectxfn, 89 | user=author, 90 | date=date, 91 | extra=kwargs) 92 | 93 | loc = lambda u: tolocal(u.encode('utf-8')) 94 | 95 | # injecting given _repo params 96 | commit_ctx._text = loc(message) 97 | commit_ctx._user = loc(author) 98 | commit_ctx._date = date 99 | 100 | # TODO: Catch exceptions! 101 | n = self.repository._repo.commitctx(commit_ctx) 102 | # Returns mercurial node 103 | self._commit_ctx = commit_ctx # For reference 104 | # Update vcs repository object & recreate mercurial _repo 105 | # new_ctx = self.repository._repo[node] 106 | # new_tip = self.repository.get_changeset(new_ctx.hex()) 107 | new_id = hex(n) 108 | self.repository.revisions.append(new_id) 109 | self._repo = self.repository._get_repo(create=False) 110 | self.repository.branches = self.repository._get_branches() 111 | tip = self.repository.get_changeset() 112 | self.reset() 113 | return tip 114 | -------------------------------------------------------------------------------- /vcs/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides some useful tools for ``vcs`` like annotate/diff html 3 | output. It also includes some internal helpers. 4 | """ 5 | import sys 6 | import time 7 | import datetime 8 | 9 | 10 | def makedate(): 11 | lt = time.localtime() 12 | if lt[8] == 1 and time.daylight: 13 | tz = time.altzone 14 | else: 15 | tz = time.timezone 16 | return time.mktime(lt), tz 17 | 18 | 19 | def aslist(obj, sep=None, strip=True): 20 | """ 21 | Returns given string separated by sep as list 22 | 23 | :param obj: 24 | :param sep: 25 | :param strip: 26 | """ 27 | if isinstance(obj, (basestring)): 28 | lst = obj.split(sep) 29 | if strip: 30 | lst = [v.strip() for v in lst] 31 | return lst 32 | elif isinstance(obj, (list, tuple)): 33 | return obj 34 | elif obj is None: 35 | return [] 36 | else: 37 | return [obj] 38 | 39 | 40 | def date_fromtimestamp(unixts, tzoffset=0): 41 | """ 42 | Makes a local datetime object out of unix timestamp 43 | 44 | :param unixts: 45 | :param tzoffset: 46 | """ 47 | 48 | return datetime.datetime.fromtimestamp(float(unixts)) 49 | 50 | 51 | def safe_int(val, default=None): 52 | """ 53 | Returns int() of val if val is not convertable to int use default 54 | instead 55 | 56 | :param val: 57 | :param default: 58 | """ 59 | 60 | try: 61 | val = int(val) 62 | except (ValueError, TypeError): 63 | val = default 64 | 65 | return val 66 | 67 | 68 | def safe_unicode(str_, from_encoding=None): 69 | """ 70 | safe unicode function. Does few trick to turn str_ into unicode 71 | 72 | In case of UnicodeDecode error we try to return it with encoding detected 73 | by chardet library if it fails fallback to unicode with errors replaced 74 | 75 | :param str_: string to decode 76 | :rtype: unicode 77 | :returns: unicode object 78 | """ 79 | if isinstance(str_, unicode): 80 | return str_ 81 | 82 | if not from_encoding: 83 | from vcs.conf import settings 84 | from_encoding = settings.DEFAULT_ENCODINGS 85 | 86 | if not isinstance(from_encoding, (list, tuple)): 87 | from_encoding = [from_encoding] 88 | 89 | try: 90 | return unicode(str_) 91 | except UnicodeDecodeError: 92 | pass 93 | 94 | for enc in from_encoding: 95 | try: 96 | return unicode(str_, enc) 97 | except UnicodeDecodeError: 98 | pass 99 | 100 | try: 101 | import chardet 102 | encoding = chardet.detect(str_)['encoding'] 103 | if encoding is None: 104 | raise Exception() 105 | return str_.decode(encoding) 106 | except (ImportError, UnicodeDecodeError, Exception): 107 | return unicode(str_, from_encoding[0], 'replace') 108 | 109 | 110 | def safe_str(unicode_, to_encoding=None): 111 | """ 112 | safe str function. Does few trick to turn unicode_ into string 113 | 114 | In case of UnicodeEncodeError we try to return it with encoding detected 115 | by chardet library if it fails fallback to string with errors replaced 116 | 117 | :param unicode_: unicode to encode 118 | :rtype: str 119 | :returns: str object 120 | """ 121 | 122 | # if it's not basestr cast to str 123 | if not isinstance(unicode_, basestring): 124 | return str(unicode_) 125 | 126 | if isinstance(unicode_, str): 127 | return unicode_ 128 | 129 | if not to_encoding: 130 | from vcs.conf import settings 131 | to_encoding = settings.DEFAULT_ENCODINGS 132 | 133 | if not isinstance(to_encoding, (list, tuple)): 134 | to_encoding = [to_encoding] 135 | 136 | for enc in to_encoding: 137 | try: 138 | return unicode_.encode(enc) 139 | except UnicodeEncodeError: 140 | pass 141 | 142 | try: 143 | import chardet 144 | encoding = chardet.detect(unicode_)['encoding'] 145 | if encoding is None: 146 | raise UnicodeEncodeError() 147 | 148 | return unicode_.encode(encoding) 149 | except (ImportError, UnicodeEncodeError): 150 | return unicode_.encode(to_encoding[0], 'replace') 151 | 152 | return safe_str 153 | 154 | 155 | def author_email(author): 156 | """ 157 | returns email address of given author. 158 | If any of <,> sign are found, it fallbacks to regex findall() 159 | and returns first found result or empty string 160 | 161 | Regex taken from http://www.regular-expressions.info/email.html 162 | """ 163 | import re 164 | r = author.find('>') 165 | l = author.find('<') 166 | 167 | if l == -1 or r == -1: 168 | # fallback to regex match of email out of a string 169 | email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!""" 170 | r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z""" 171 | r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]""" 172 | r"""*[a-z0-9])?""", re.IGNORECASE) 173 | m = re.findall(email_re, author) 174 | return m[0] if m else '' 175 | 176 | return author[l + 1:r].strip() 177 | 178 | 179 | def author_name(author): 180 | """ 181 | get name of author, or else username. 182 | It'll try to find an email in the author string and just cut it off 183 | to get the username 184 | """ 185 | 186 | if not '@' in author: 187 | return author 188 | else: 189 | return author.replace(author_email(author), '').replace('<', '')\ 190 | .replace('>', '').strip() 191 | -------------------------------------------------------------------------------- /docs/theme/ADC/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {%- block doctype -%} 3 | 5 | {%- endblock %} 6 | {%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} 7 | {%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} 8 | {%- block linktags %} 9 | {%- if hasdoc('about') %} 10 | 11 | {%- endif %} 12 | {%- if hasdoc('genindex') %} 13 | 14 | {%- endif %} 15 | {%- if hasdoc('search') %} 16 | 17 | {%- endif %} 18 | {%- if hasdoc('copyright') %} 19 | 20 | {%- endif %} 21 | 22 | {%- if parents %} 23 | 24 | {%- endif %} 25 | {%- if next %} 26 | 27 | {%- endif %} 28 | {%- if prev %} 29 | 30 | {%- endif %} 31 | 32 | {%- endblock %} 33 | {%- block extrahead %} {% endblock %} 34 | {%- block header %}{% endblock %} 35 | {%- block relbar1 %} 36 |
37 |

{{docstitle}}

38 |
39 | 50 | {% endblock %} 51 | 52 | {%- block sidebar1 %} 53 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 54 |
55 |
56 | {%- block sidebarlogo %} 57 | {%- if logo %} 58 | 61 | {%- endif %} 62 | {%- endblock %} 63 | {%- block sidebartoc %} 64 | 65 | {{ toctree() }} 66 | {%- endblock %} 67 | {%- block sidebarrel %} 68 | {%- endblock %} 69 | {%- block sidebarsourcelink %} 70 | {%- if show_source and has_source and sourcename %} 71 |

{{ _('This Page') }}

72 | 76 | {%- endif %} 77 | {%- endblock %} 78 | {%- if customsidebar %} 79 | {% include customsidebar %} 80 | {%- endif %} 81 | {%- block sidebarsearch %} 82 | {%- if pagename != "search" %} 83 | 99 | 100 | {%- endif %} 101 | {%- endblock %} 102 |
103 |
104 | {%- endif %}{% endif %} 105 | 106 | {% endblock %} 107 | {%- block document %} 108 |
109 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 110 |
111 | {%- endif %}{% endif %} 112 |
113 | {% block body %} {% endblock %} 114 |
115 | {%- if not embedded %}{% if not theme_nosidebar|tobool %} 116 |
117 | {%- endif %}{% endif %} 118 |
119 | 133 | {%- endblock %} 134 | {%- block sidebar2 %}{% endblock %} 135 | {%- block relbar2 %}{% endblock %} 136 | {%- block footer %} 137 | 144 | 145 | {%- endblock %} 146 | -------------------------------------------------------------------------------- /vcs/tests/test_repository.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import datetime 3 | from vcs.tests.base import BackendTestMixin 4 | from vcs.tests.conf import SCM_TESTS 5 | from vcs.tests.conf import TEST_USER_CONFIG_FILE 6 | from vcs.nodes import FileNode 7 | from vcs.utils.compat import unittest 8 | from vcs.exceptions import ChangesetDoesNotExistError 9 | 10 | 11 | class RepositoryBaseTest(BackendTestMixin): 12 | recreate_repo_per_test = False 13 | 14 | @classmethod 15 | def _get_commits(cls): 16 | return super(RepositoryBaseTest, cls)._get_commits()[:1] 17 | 18 | def test_get_config_value(self): 19 | self.assertEqual(self.repo.get_config_value('universal', 'foo', 20 | TEST_USER_CONFIG_FILE), 'bar') 21 | 22 | def test_get_config_value_defaults_to_None(self): 23 | self.assertEqual(self.repo.get_config_value('universal', 'nonexist', 24 | TEST_USER_CONFIG_FILE), None) 25 | 26 | def test_get_user_name(self): 27 | self.assertEqual(self.repo.get_user_name(TEST_USER_CONFIG_FILE), 28 | 'Foo Bar') 29 | 30 | def test_get_user_email(self): 31 | self.assertEqual(self.repo.get_user_email(TEST_USER_CONFIG_FILE), 32 | 'foo.bar@example.com') 33 | 34 | def test_repo_equality(self): 35 | self.assertTrue(self.repo == self.repo) 36 | 37 | def test_repo_equality_broken_object(self): 38 | import copy 39 | _repo = copy.copy(self.repo) 40 | delattr(_repo, 'path') 41 | self.assertTrue(self.repo != _repo) 42 | 43 | def test_repo_equality_other_object(self): 44 | class dummy(object): 45 | path = self.repo.path 46 | self.assertTrue(self.repo != dummy()) 47 | 48 | 49 | class RepositoryGetDiffTest(BackendTestMixin): 50 | 51 | @classmethod 52 | def _get_commits(cls): 53 | commits = [ 54 | { 55 | 'message': 'Initial commit', 56 | 'author': 'Joe Doe ', 57 | 'date': datetime.datetime(2010, 1, 1, 20), 58 | 'added': [ 59 | FileNode('foobar', content='foobar'), 60 | FileNode('foobar2', content='foobar2'), 61 | ], 62 | }, 63 | { 64 | 'message': 'Changed foobar, added foobar3', 65 | 'author': 'Jane Doe ', 66 | 'date': datetime.datetime(2010, 1, 1, 21), 67 | 'added': [ 68 | FileNode('foobar3', content='foobar3'), 69 | ], 70 | 'changed': [ 71 | FileNode('foobar', 'FOOBAR'), 72 | ], 73 | }, 74 | { 75 | 'message': 'Removed foobar, changed foobar3', 76 | 'author': 'Jane Doe ', 77 | 'date': datetime.datetime(2010, 1, 1, 22), 78 | 'changed': [ 79 | FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'), 80 | ], 81 | 'removed': [FileNode('foobar')], 82 | }, 83 | ] 84 | return commits 85 | 86 | def test_raise_for_wrong(self): 87 | with self.assertRaises(ChangesetDoesNotExistError): 88 | self.repo.get_diff('a' * 40, 'b' * 40) 89 | 90 | 91 | class GitRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase): 92 | backend_alias = 'git' 93 | 94 | def test_initial_commit_diff(self): 95 | initial_rev = self.repo.revisions[0] 96 | self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar 97 | new file mode 100644 98 | index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632 99 | --- /dev/null 100 | +++ b/foobar 101 | @@ -0,0 +1 @@ 102 | +foobar 103 | \ No newline at end of file 104 | diff --git a/foobar2 b/foobar2 105 | new file mode 100644 106 | index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783 107 | --- /dev/null 108 | +++ b/foobar2 109 | @@ -0,0 +1 @@ 110 | +foobar2 111 | \ No newline at end of file 112 | ''') 113 | 114 | def test_second_changeset_diff(self): 115 | revs = self.repo.revisions 116 | self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar 117 | index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644 118 | --- a/foobar 119 | +++ b/foobar 120 | @@ -1 +1 @@ 121 | -foobar 122 | \ No newline at end of file 123 | +FOOBAR 124 | \ No newline at end of file 125 | diff --git a/foobar3 b/foobar3 126 | new file mode 100644 127 | index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0 128 | --- /dev/null 129 | +++ b/foobar3 130 | @@ -0,0 +1 @@ 131 | +foobar3 132 | \ No newline at end of file 133 | ''') 134 | 135 | def test_third_changeset_diff(self): 136 | revs = self.repo.revisions 137 | self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar 138 | deleted file mode 100644 139 | index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000 140 | --- a/foobar 141 | +++ /dev/null 142 | @@ -1 +0,0 @@ 143 | -FOOBAR 144 | \ No newline at end of file 145 | diff --git a/foobar3 b/foobar3 146 | index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644 147 | --- a/foobar3 148 | +++ b/foobar3 149 | @@ -1 +1,3 @@ 150 | -foobar3 151 | \ No newline at end of file 152 | +FOOBAR 153 | +FOOBAR 154 | +FOOBAR 155 | ''') 156 | 157 | 158 | class HgRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase): 159 | backend_alias = 'hg' 160 | 161 | def test_initial_commit_diff(self): 162 | initial_rev = self.repo.revisions[0] 163 | self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar 164 | new file mode 100755 165 | --- /dev/null 166 | +++ b/foobar 167 | @@ -0,0 +1,1 @@ 168 | +foobar 169 | \ No newline at end of file 170 | diff --git a/foobar2 b/foobar2 171 | new file mode 100755 172 | --- /dev/null 173 | +++ b/foobar2 174 | @@ -0,0 +1,1 @@ 175 | +foobar2 176 | \ No newline at end of file 177 | ''') 178 | 179 | def test_second_changeset_diff(self): 180 | revs = self.repo.revisions 181 | self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar 182 | --- a/foobar 183 | +++ b/foobar 184 | @@ -1,1 +1,1 @@ 185 | -foobar 186 | \ No newline at end of file 187 | +FOOBAR 188 | \ No newline at end of file 189 | diff --git a/foobar3 b/foobar3 190 | new file mode 100755 191 | --- /dev/null 192 | +++ b/foobar3 193 | @@ -0,0 +1,1 @@ 194 | +foobar3 195 | \ No newline at end of file 196 | ''') 197 | 198 | def test_third_changeset_diff(self): 199 | revs = self.repo.revisions 200 | self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar 201 | deleted file mode 100755 202 | --- a/foobar 203 | +++ /dev/null 204 | @@ -1,1 +0,0 @@ 205 | -FOOBAR 206 | \ No newline at end of file 207 | diff --git a/foobar3 b/foobar3 208 | --- a/foobar3 209 | +++ b/foobar3 210 | @@ -1,1 +1,3 @@ 211 | -foobar3 212 | \ No newline at end of file 213 | +FOOBAR 214 | +FOOBAR 215 | +FOOBAR 216 | ''') 217 | 218 | 219 | # For each backend create test case class 220 | for alias in SCM_TESTS: 221 | attrs = { 222 | 'backend_alias': alias, 223 | } 224 | cls_name = alias.capitalize() + RepositoryBaseTest.__name__ 225 | bases = (RepositoryBaseTest, unittest.TestCase) 226 | globals()[cls_name] = type(cls_name, bases, attrs) 227 | 228 | if __name__ == '__main__': 229 | unittest.main() 230 | -------------------------------------------------------------------------------- /vcs/utils/termcolors.py: -------------------------------------------------------------------------------- 1 | """ 2 | termcolors.py 3 | 4 | Grabbed from Django (http://www.djangoproject.com) 5 | """ 6 | 7 | color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') 8 | foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) 9 | background = dict([(color_names[x], '4%s' % x) for x in range(8)]) 10 | 11 | RESET = '0' 12 | opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} 13 | 14 | def colorize(text='', opts=(), **kwargs): 15 | """ 16 | Returns your text, enclosed in ANSI graphics codes. 17 | 18 | Depends on the keyword arguments 'fg' and 'bg', and the contents of 19 | the opts tuple/list. 20 | 21 | Returns the RESET code if no parameters are given. 22 | 23 | Valid colors: 24 | 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' 25 | 26 | Valid options: 27 | 'bold' 28 | 'underscore' 29 | 'blink' 30 | 'reverse' 31 | 'conceal' 32 | 'noreset' - string will not be auto-terminated with the RESET code 33 | 34 | Examples: 35 | colorize('hello', fg='red', bg='blue', opts=('blink',)) 36 | colorize() 37 | colorize('goodbye', opts=('underscore',)) 38 | print colorize('first line', fg='red', opts=('noreset',)) 39 | print 'this should be red too' 40 | print colorize('and so should this') 41 | print 'this should not be red' 42 | """ 43 | code_list = [] 44 | if text == '' and len(opts) == 1 and opts[0] == 'reset': 45 | return '\x1b[%sm' % RESET 46 | for k, v in kwargs.iteritems(): 47 | if k == 'fg': 48 | code_list.append(foreground[v]) 49 | elif k == 'bg': 50 | code_list.append(background[v]) 51 | for o in opts: 52 | if o in opt_dict: 53 | code_list.append(opt_dict[o]) 54 | if 'noreset' not in opts: 55 | text = text + '\x1b[%sm' % RESET 56 | return ('\x1b[%sm' % ';'.join(code_list)) + text 57 | 58 | def make_style(opts=(), **kwargs): 59 | """ 60 | Returns a function with default parameters for colorize() 61 | 62 | Example: 63 | bold_red = make_style(opts=('bold',), fg='red') 64 | print bold_red('hello') 65 | KEYWORD = make_style(fg='yellow') 66 | COMMENT = make_style(fg='blue', opts=('bold',)) 67 | """ 68 | return lambda text: colorize(text, opts, **kwargs) 69 | 70 | NOCOLOR_PALETTE = 'nocolor' 71 | DARK_PALETTE = 'dark' 72 | LIGHT_PALETTE = 'light' 73 | 74 | PALETTES = { 75 | NOCOLOR_PALETTE: { 76 | 'ERROR': {}, 77 | 'NOTICE': {}, 78 | 'SQL_FIELD': {}, 79 | 'SQL_COLTYPE': {}, 80 | 'SQL_KEYWORD': {}, 81 | 'SQL_TABLE': {}, 82 | 'HTTP_INFO': {}, 83 | 'HTTP_SUCCESS': {}, 84 | 'HTTP_REDIRECT': {}, 85 | 'HTTP_NOT_MODIFIED': {}, 86 | 'HTTP_BAD_REQUEST': {}, 87 | 'HTTP_NOT_FOUND': {}, 88 | 'HTTP_SERVER_ERROR': {}, 89 | }, 90 | DARK_PALETTE: { 91 | 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, 92 | 'NOTICE': { 'fg': 'red' }, 93 | 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, 94 | 'SQL_COLTYPE': { 'fg': 'green' }, 95 | 'SQL_KEYWORD': { 'fg': 'yellow' }, 96 | 'SQL_TABLE': { 'opts': ('bold',) }, 97 | 'HTTP_INFO': { 'opts': ('bold',) }, 98 | 'HTTP_SUCCESS': { }, 99 | 'HTTP_REDIRECT': { 'fg': 'green' }, 100 | 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' }, 101 | 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, 102 | 'HTTP_NOT_FOUND': { 'fg': 'yellow' }, 103 | 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, 104 | }, 105 | LIGHT_PALETTE: { 106 | 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, 107 | 'NOTICE': { 'fg': 'red' }, 108 | 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, 109 | 'SQL_COLTYPE': { 'fg': 'green' }, 110 | 'SQL_KEYWORD': { 'fg': 'blue' }, 111 | 'SQL_TABLE': { 'opts': ('bold',) }, 112 | 'HTTP_INFO': { 'opts': ('bold',) }, 113 | 'HTTP_SUCCESS': { }, 114 | 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) }, 115 | 'HTTP_NOT_MODIFIED': { 'fg': 'green' }, 116 | 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) }, 117 | 'HTTP_NOT_FOUND': { 'fg': 'red' }, 118 | 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) }, 119 | } 120 | } 121 | DEFAULT_PALETTE = DARK_PALETTE 122 | 123 | def parse_color_setting(config_string): 124 | """Parse a DJANGO_COLORS environment variable to produce the system palette 125 | 126 | The general form of a pallete definition is: 127 | 128 | "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option" 129 | 130 | where: 131 | palette is a named palette; one of 'light', 'dark', or 'nocolor'. 132 | role is a named style used by Django 133 | fg is a background color. 134 | bg is a background color. 135 | option is a display options. 136 | 137 | Specifying a named palette is the same as manually specifying the individual 138 | definitions for each role. Any individual definitions following the pallete 139 | definition will augment the base palette definition. 140 | 141 | Valid roles: 142 | 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table', 143 | 'http_info', 'http_success', 'http_redirect', 'http_bad_request', 144 | 'http_not_found', 'http_server_error' 145 | 146 | Valid colors: 147 | 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' 148 | 149 | Valid options: 150 | 'bold', 'underscore', 'blink', 'reverse', 'conceal' 151 | 152 | """ 153 | if not config_string: 154 | return PALETTES[DEFAULT_PALETTE] 155 | 156 | # Split the color configuration into parts 157 | parts = config_string.lower().split(';') 158 | palette = PALETTES[NOCOLOR_PALETTE].copy() 159 | for part in parts: 160 | if part in PALETTES: 161 | # A default palette has been specified 162 | palette.update(PALETTES[part]) 163 | elif '=' in part: 164 | # Process a palette defining string 165 | definition = {} 166 | 167 | # Break the definition into the role, 168 | # plus the list of specific instructions. 169 | # The role must be in upper case 170 | role, instructions = part.split('=') 171 | role = role.upper() 172 | 173 | styles = instructions.split(',') 174 | styles.reverse() 175 | 176 | # The first instruction can contain a slash 177 | # to break apart fg/bg. 178 | colors = styles.pop().split('/') 179 | colors.reverse() 180 | fg = colors.pop() 181 | if fg in color_names: 182 | definition['fg'] = fg 183 | if colors and colors[-1] in color_names: 184 | definition['bg'] = colors[-1] 185 | 186 | # All remaining instructions are options 187 | opts = tuple(s for s in styles if s in opt_dict.keys()) 188 | if opts: 189 | definition['opts'] = opts 190 | 191 | # The nocolor palette has all available roles. 192 | # Use that palette as the basis for determining 193 | # if the role is valid. 194 | if role in PALETTES[NOCOLOR_PALETTE] and definition: 195 | palette[role] = definition 196 | 197 | # If there are no colors specified, return the empty palette. 198 | if palette == PALETTES[NOCOLOR_PALETTE]: 199 | return None 200 | return palette 201 | -------------------------------------------------------------------------------- /vcs/utils/annotate.py: -------------------------------------------------------------------------------- 1 | import StringIO 2 | 3 | from pygments.formatters import HtmlFormatter 4 | from pygments import highlight 5 | 6 | from vcs.exceptions import VCSError 7 | from vcs.nodes import FileNode 8 | 9 | 10 | def annotate_highlight(filenode, annotate_from_changeset_func=None, 11 | order=None, headers=None, **options): 12 | """ 13 | Returns html portion containing annotated table with 3 columns: line 14 | numbers, changeset information and pygmentized line of code. 15 | 16 | :param filenode: FileNode object 17 | :param annotate_from_changeset_func: function taking changeset and 18 | returning single annotate cell; needs break line at the end 19 | :param order: ordered sequence of ``ls`` (line numbers column), 20 | ``annotate`` (annotate column), ``code`` (code column); Default is 21 | ``['ls', 'annotate', 'code']`` 22 | :param headers: dictionary with headers (keys are whats in ``order`` 23 | parameter) 24 | """ 25 | options['linenos'] = True 26 | formatter = AnnotateHtmlFormatter(filenode=filenode, order=order, 27 | headers=headers, 28 | annotate_from_changeset_func=annotate_from_changeset_func, **options) 29 | lexer = filenode.lexer 30 | highlighted = highlight(filenode.content, lexer, formatter) 31 | return highlighted 32 | 33 | 34 | class AnnotateHtmlFormatter(HtmlFormatter): 35 | 36 | def __init__(self, filenode, annotate_from_changeset_func=None, 37 | order=None, **options): 38 | """ 39 | If ``annotate_from_changeset_func`` is passed it should be a function 40 | which returns string from the given changeset. For example, we may pass 41 | following function as ``annotate_from_changeset_func``:: 42 | 43 | def changeset_to_anchor(changeset): 44 | return '%s\n' %\ 45 | (changeset.id, changeset.id) 46 | 47 | :param annotate_from_changeset_func: see above 48 | :param order: (default: ``['ls', 'annotate', 'code']``); order of 49 | columns; 50 | :param options: standard pygment's HtmlFormatter options, there is 51 | extra option tough, ``headers``. For instance we can pass:: 52 | 53 | formatter = AnnotateHtmlFormatter(filenode, headers={ 54 | 'ls': '#', 55 | 'annotate': 'Annotate', 56 | 'code': 'Code', 57 | }) 58 | 59 | """ 60 | super(AnnotateHtmlFormatter, self).__init__(**options) 61 | self.annotate_from_changeset_func = annotate_from_changeset_func 62 | self.order = order or ('ls', 'annotate', 'code') 63 | headers = options.pop('headers', None) 64 | if headers and not ('ls' in headers and 'annotate' in headers and 65 | 'code' in headers): 66 | raise ValueError("If headers option dict is specified it must " 67 | "all 'ls', 'annotate' and 'code' keys") 68 | self.headers = headers 69 | if isinstance(filenode, FileNode): 70 | self.filenode = filenode 71 | else: 72 | raise VCSError("This formatter expect FileNode parameter, not %r" 73 | % type(filenode)) 74 | 75 | def annotate_from_changeset(self, changeset): 76 | """ 77 | Returns full html line for single changeset per annotated line. 78 | """ 79 | if self.annotate_from_changeset_func: 80 | return self.annotate_from_changeset_func(changeset) 81 | else: 82 | return ''.join((changeset.id, '\n')) 83 | 84 | def _wrap_tablelinenos(self, inner): 85 | dummyoutfile = StringIO.StringIO() 86 | lncount = 0 87 | for t, line in inner: 88 | if t: 89 | lncount += 1 90 | dummyoutfile.write(line) 91 | 92 | fl = self.linenostart 93 | mw = len(str(lncount + fl - 1)) 94 | sp = self.linenospecial 95 | st = self.linenostep 96 | la = self.lineanchors 97 | aln = self.anchorlinenos 98 | if sp: 99 | lines = [] 100 | 101 | for i in range(fl, fl + lncount): 102 | if i % st == 0: 103 | if i % sp == 0: 104 | if aln: 105 | lines.append('' 106 | '%*d' % 107 | (la, i, mw, i)) 108 | else: 109 | lines.append('' 110 | '%*d' % (mw, i)) 111 | else: 112 | if aln: 113 | lines.append('' 114 | '%*d' % (la, i, mw, i)) 115 | else: 116 | lines.append('%*d' % (mw, i)) 117 | else: 118 | lines.append('') 119 | ls = '\n'.join(lines) 120 | else: 121 | lines = [] 122 | for i in range(fl, fl + lncount): 123 | if i % st == 0: 124 | if aln: 125 | lines.append('%*d' \ 126 | % (la, i, mw, i)) 127 | else: 128 | lines.append('%*d' % (mw, i)) 129 | else: 130 | lines.append('') 131 | ls = '\n'.join(lines) 132 | 133 | annotate_changesets = [tup[1] for tup in self.filenode.annotate] 134 | # If pygments cropped last lines break we need do that too 135 | ln_cs = len(annotate_changesets) 136 | ln_ = len(ls.splitlines()) 137 | if ln_cs > ln_: 138 | annotate_changesets = annotate_changesets[:ln_ - ln_cs] 139 | annotate = ''.join((self.annotate_from_changeset(changeset) 140 | for changeset in annotate_changesets)) 141 | # in case you wonder about the seemingly redundant
here: 142 | # since the content in the other cell also is wrapped in a div, 143 | # some browsers in some configurations seem to mess up the formatting. 144 | ''' 145 | yield 0, ('' % self.cssclass + 146 | '' + 148 | '
' +
147 |                   ls + '
') 149 | yield 0, dummyoutfile.getvalue() 150 | yield 0, '
' 151 | 152 | ''' 153 | headers_row = [] 154 | if self.headers: 155 | headers_row = [''] 156 | for key in self.order: 157 | td = ''.join(('', self.headers[key], '')) 158 | headers_row.append(td) 159 | headers_row.append('') 160 | 161 | body_row_start = [''] 162 | for key in self.order: 163 | if key == 'ls': 164 | body_row_start.append( 165 | '
' +
166 |                     ls + '
') 167 | elif key == 'annotate': 168 | body_row_start.append( 169 | '
' +
170 |                     annotate + '
') 171 | elif key == 'code': 172 | body_row_start.append('') 173 | yield 0, ('' % self.cssclass + 174 | ''.join(headers_row) + 175 | ''.join(body_row_start) 176 | ) 177 | yield 0, dummyoutfile.getvalue() 178 | yield 0, '
' 179 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-projector documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Feb 18 23:18:28 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing 7 | # dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__)))) 22 | sys.path.append(os.path.abspath('..')) 23 | vcs = __import__('vcs') 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'exts'] 30 | try: 31 | import rst2pdf 32 | if rst2pdf.version >= '0.16': 33 | extensions.append('rst2pdf.pdfbuilder') 34 | except ImportError: 35 | print "[NOTE] In order to build PDF you need rst2pdf with version >=0.16" 36 | 37 | autoclass_content = "both" 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'vcs' 53 | copyright = u'2010, Marcin Kuzminski & Lukasz Balcerzak' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = vcs.get_version() 61 | # The full version, including alpha/beta/rc tags. 62 | release = vcs.__version__ 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of documents that shouldn't be included in the build. 75 | #unused_docs = [] 76 | 77 | # List of directories, relative to source directory, that shouldn't be searched 78 | # for source files. 79 | exclude_trees = ['build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all documents. 82 | #default_role = None 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | #add_function_parentheses = True 86 | 87 | # If true, the current module name will be prepended to all description 88 | # unit titles (such as .. function::). 89 | #add_module_names = True 90 | 91 | # If true, sectionauthor and moduleauthor directives will be shown in the 92 | # output. They are ignored by default. 93 | #show_authors = False 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | pygments_style = 'sphinx' 97 | 98 | # A list of ignored prefixes for module index sorting. 99 | #modindex_common_prefix = [] 100 | 101 | 102 | # -- Options for HTML output --------------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. Major themes that come with 105 | # Sphinx are currently 'default' and 'sphinxdoc'. 106 | html_theme = 'ADC' 107 | 108 | # Theme options are theme-specific and customize the look and feel of a theme 109 | # further. For a list of options available for each theme, see the 110 | # documentation. 111 | #html_theme_options = {} 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | html_theme_path = ['theme'] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | #html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | #html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | #html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | #html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = [] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | #html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | #html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | #html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | #html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | #html_use_modindex = True 154 | 155 | # If false, no index is generated. 156 | #html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | #html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | #html_show_sourcelink = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 170 | #html_file_suffix = '' 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'vcsdoc' 174 | 175 | 176 | # -- Options for LaTeX output -------------------------------------------------- 177 | 178 | # The paper size ('letter' or 'a4'). 179 | #latex_paper_size = 'letter' 180 | 181 | # The font size ('10pt', '11pt' or '12pt'). 182 | #latex_font_size = '10pt' 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass [howto/manual]). 186 | latex_documents = [ 187 | ('index', 'vcs.tex', u'vcs Documentation', 188 | u'Marcin Kuzminski & Lukasz Balcerzak', 'manual'), 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | #latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | #latex_use_parts = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_use_modindex = True 207 | 208 | pdf_documents = [ 209 | ('index', u'vcs', u'vcs Documentation', 210 | u'Marcin Kuzminski & Lukasz Balcerzak') , 211 | ] 212 | pdf_stylesheets = ['sphinx', 'kerning', 'a4'] 213 | pdf_break_level = 1 214 | pdf_inline_footnotes = True 215 | -------------------------------------------------------------------------------- /vcs/tests/test_nodes.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import stat 4 | from vcs.nodes import DirNode 5 | from vcs.nodes import FileNode 6 | from vcs.nodes import Node 7 | from vcs.nodes import NodeError 8 | from vcs.nodes import NodeKind 9 | from vcs.utils.compat import unittest 10 | 11 | 12 | class NodeBasicTest(unittest.TestCase): 13 | 14 | def test_init(self): 15 | """ 16 | Cannot innitialize Node objects with path with slash at the beginning. 17 | """ 18 | wrong_paths = ( 19 | '/foo', 20 | '/foo/bar' 21 | ) 22 | for path in wrong_paths: 23 | self.assertRaises(NodeError, Node, path, NodeKind.FILE) 24 | 25 | wrong_paths = ( 26 | '/foo/', 27 | '/foo/bar/' 28 | ) 29 | for path in wrong_paths: 30 | self.assertRaises(NodeError, Node, path, NodeKind.DIR) 31 | 32 | def test_name(self): 33 | node = Node('', NodeKind.DIR) 34 | self.assertEqual(node.name, '') 35 | 36 | node = Node('path', NodeKind.FILE) 37 | self.assertEqual(node.name, 'path') 38 | 39 | node = Node('path/', NodeKind.DIR) 40 | self.assertEqual(node.name, 'path') 41 | 42 | node = Node('some/path', NodeKind.FILE) 43 | self.assertEqual(node.name, 'path') 44 | 45 | node = Node('some/path/', NodeKind.DIR) 46 | self.assertEqual(node.name, 'path') 47 | 48 | def test_root_node(self): 49 | self.assertRaises(NodeError, Node, '', NodeKind.FILE) 50 | 51 | def test_kind_setter(self): 52 | node = Node('', NodeKind.DIR) 53 | self.assertRaises(NodeError, setattr, node, 'kind', NodeKind.FILE) 54 | 55 | def _test_parent_path(self, node_path, expected_parent_path): 56 | """ 57 | Tests if node's parent path are properly computed. 58 | """ 59 | node = Node(node_path, NodeKind.DIR) 60 | parent_path = node.get_parent_path() 61 | self.assertTrue(parent_path.endswith('/') or \ 62 | node.is_root() and parent_path == '') 63 | self.assertEqual(parent_path, expected_parent_path, 64 | "Node's path is %r and parent path is %r but should be %r" 65 | % (node.path, parent_path, expected_parent_path)) 66 | 67 | def test_parent_path(self): 68 | test_paths = ( 69 | # (node_path, expected_parent_path) 70 | ('', ''), 71 | ('some/path/', 'some/'), 72 | ('some/longer/path/', 'some/longer/'), 73 | ) 74 | for node_path, expected_parent_path in test_paths: 75 | self._test_parent_path(node_path, expected_parent_path) 76 | 77 | ''' 78 | def _test_trailing_slash(self, path): 79 | if not path.endswith('/'): 80 | self.fail("Trailing slash tests needs paths to end with slash") 81 | for kind in NodeKind.FILE, NodeKind.DIR: 82 | self.assertRaises(NodeError, Node, path=path, kind=kind) 83 | 84 | def test_trailing_slash(self): 85 | for path in ('/', 'foo/', 'foo/bar/', 'foo/bar/biz/'): 86 | self._test_trailing_slash(path) 87 | ''' 88 | 89 | def test_is_file(self): 90 | node = Node('any', NodeKind.FILE) 91 | self.assertTrue(node.is_file()) 92 | 93 | node = FileNode('any') 94 | self.assertTrue(node.is_file()) 95 | self.assertRaises(AttributeError, getattr, node, 'nodes') 96 | 97 | def test_is_dir(self): 98 | node = Node('any_dir', NodeKind.DIR) 99 | self.assertTrue(node.is_dir()) 100 | 101 | node = DirNode('any_dir') 102 | 103 | self.assertTrue(node.is_dir()) 104 | self.assertRaises(NodeError, getattr, node, 'content') 105 | 106 | def test_dir_node_iter(self): 107 | nodes = [ 108 | DirNode('docs'), 109 | DirNode('tests'), 110 | FileNode('bar'), 111 | FileNode('foo'), 112 | FileNode('readme.txt'), 113 | FileNode('setup.py'), 114 | ] 115 | dirnode = DirNode('', nodes=nodes) 116 | for node in dirnode: 117 | node == dirnode.get_node(node.path) 118 | 119 | def test_node_state(self): 120 | """ 121 | Without link to changeset nodes should raise NodeError. 122 | """ 123 | node = FileNode('anything') 124 | self.assertRaises(NodeError, getattr, node, 'state') 125 | node = DirNode('anything') 126 | self.assertRaises(NodeError, getattr, node, 'state') 127 | 128 | def test_file_node_stat(self): 129 | node = FileNode('foobar', 'empty... almost') 130 | mode = node.mode # default should be 0100644 131 | self.assertTrue(mode & stat.S_IRUSR) 132 | self.assertTrue(mode & stat.S_IWUSR) 133 | self.assertTrue(mode & stat.S_IRGRP) 134 | self.assertTrue(mode & stat.S_IROTH) 135 | self.assertFalse(mode & stat.S_IWGRP) 136 | self.assertFalse(mode & stat.S_IWOTH) 137 | self.assertFalse(mode & stat.S_IXUSR) 138 | self.assertFalse(mode & stat.S_IXGRP) 139 | self.assertFalse(mode & stat.S_IXOTH) 140 | 141 | def test_file_node_is_executable(self): 142 | node = FileNode('foobar', 'empty... almost', mode=0100755) 143 | self.assertTrue(node.is_executable()) 144 | 145 | node = FileNode('foobar', 'empty... almost', mode=0100500) 146 | self.assertTrue(node.is_executable()) 147 | 148 | node = FileNode('foobar', 'empty... almost', mode=0100644) 149 | self.assertFalse(node.is_executable()) 150 | 151 | def test_mimetype(self): 152 | py_node = FileNode('test.py') 153 | tar_node = FileNode('test.tar.gz') 154 | 155 | ext = 'CustomExtension' 156 | 157 | my_node2 = FileNode('myfile2') 158 | my_node2._mimetype = [ext] 159 | 160 | my_node3 = FileNode('myfile3') 161 | my_node3._mimetype = [ext,ext] 162 | 163 | self.assertEqual(py_node.mimetype,'text/x-python') 164 | self.assertEqual(py_node.get_mimetype(),('text/x-python',None)) 165 | 166 | self.assertEqual(tar_node.mimetype,'application/x-tar') 167 | self.assertEqual(tar_node.get_mimetype(),('application/x-tar','gzip')) 168 | 169 | self.assertRaises(NodeError,my_node2.get_mimetype) 170 | 171 | self.assertEqual(my_node3.mimetype,ext) 172 | self.assertEqual(my_node3.get_mimetype(),[ext,ext]) 173 | 174 | class NodeContentTest(unittest.TestCase): 175 | 176 | def test_if_binary(self): 177 | data = """\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f??a\x00\x00\x00\x04gAMA\x00\x00\xaf?7\x05\x8a?\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq?e<\x00\x00\x025IDAT8?\xa5\x93?K\x94Q\x14\x87\x9f\xf7?Q\x1bs4?\x03\x9a\xa8?B\x02\x8b$\x10[U;i\x13?6h?&h[?"\x14j?\xa2M\x7fB\x14F\x9aQ?&\x842?\x0b\x89"\x82??!?\x9c!\x9c2l??{N\x8bW\x9dY\xb4\t/\x1c?=\x9b?}????\xa9*;9!?\x83\x91?[?\\v*?D\x04\'`EpNp\xa2X\'U?pVq"Sw.\x1e?\x08\x01D?jw????\xbc??7{|\x9b?\x89$\x01??W@\x15\x9c\x05q`Lt/\x97?\x94\xa1d?\x18~?\x18?\x18W[%\xb0?\x83??\x14\x88\x8dB?\xa6H\tL\tl\x19>/\x01`\xac\xabx?\x9cl\nx\xb0\x98\x07\x95\x88D$"q[\x19?d\x00(o\n\xa0??\x7f\xb9\xa4?\x1bF\x1f\x8e\xac\xa8?j??eUU}?.?\x9f\x8cE??x\x94??\r\xbdtoJU5"0N\x10U?\x00??V\t\x02\x9f\x81?U?\x00\x9eM\xae2?r\x9b7\x83\x82\x8aP3????.?&"?\xb7ZP \x0cJ?\x80\x15T\x95\x9a\x00??S\x8c\r?\xa1\x03\x07?\x96\x9b\xa7\xab=E??\xa4\xb3?\x19q??B\x91=\x8d??k?J\x0bV"??\xf7x?\xa1\x00?\\.\x87\x87???\x02F@D\x99],??\x10#?X\xb7=\xb9\x10?Z\x1by???cI??\x1ag?\x92\xbc?T?t[\x92\x81?<_\x17~\x92\x88?H%?\x10Q\x02\x9f\n\x81qQ\x0bm?\x1bX?\xb1AK\xa6\x9e\xb9?u\xb2?1\xbe|/\x92M@\xa2!F?\xa9>"\r\x92\x8e?>\x9a9Qv\x127?a\xac?Y?8?:??]X???9\x80\xb7?u?\x0b#BZ\x8d=\x1d?p\x00\x00\x00\x00IEND\xaeB`\x82""" 178 | filenode = FileNode('calendar.png', content=data) 179 | self.assertTrue(filenode.is_binary) 180 | 181 | 182 | if __name__ == '__main__': 183 | unittest.main() 184 | -------------------------------------------------------------------------------- /vcs/backends/git/inmemory.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import posixpath 4 | from dulwich import objects 5 | from vcs.backends.base import BaseInMemoryChangeset 6 | from vcs.exceptions import RepositoryError 7 | from vcs.utils import safe_str 8 | 9 | 10 | class GitInMemoryChangeset(BaseInMemoryChangeset): 11 | 12 | def commit(self, message, author, parents=None, branch=None, date=None, 13 | **kwargs): 14 | """ 15 | Performs in-memory commit (doesn't check workdir in any way) and 16 | returns newly created ``Changeset``. Updates repository's 17 | ``revisions``. 18 | 19 | :param message: message of the commit 20 | :param author: full username, i.e. "Joe Doe " 21 | :param parents: single parent or sequence of parents from which commit 22 | would be derieved 23 | :param date: ``datetime.datetime`` instance. Defaults to 24 | ``datetime.datetime.now()``. 25 | :param branch: branch name, as string. If none given, default backend's 26 | branch would be used. 27 | 28 | :raises ``CommitError``: if any error occurs while committing 29 | """ 30 | self.check_integrity(parents) 31 | 32 | from .repository import GitRepository 33 | if branch is None: 34 | branch = GitRepository.DEFAULT_BRANCH_NAME 35 | 36 | repo = self.repository._repo 37 | object_store = repo.object_store 38 | 39 | ENCODING = "UTF-8" 40 | DIRMOD = 040000 41 | 42 | # Create tree and populates it with blobs 43 | commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or\ 44 | objects.Tree() 45 | for node in self.added + self.changed: 46 | # Compute subdirs if needed 47 | dirpath, nodename = posixpath.split(node.path) 48 | dirnames = dirpath and dirpath.split('/') or [] 49 | parent = commit_tree 50 | ancestors = [('', parent)] 51 | 52 | # Tries to dig for the deepest existing tree 53 | while dirnames: 54 | curdir = dirnames.pop(0) 55 | try: 56 | dir_id = parent[curdir][1] 57 | except KeyError: 58 | # put curdir back into dirnames and stops 59 | dirnames.insert(0, curdir) 60 | break 61 | else: 62 | # If found, updates parent 63 | parent = self.repository._repo[dir_id] 64 | ancestors.append((curdir, parent)) 65 | # Now parent is deepest existing tree and we need to create subtrees 66 | # for dirnames (in reverse order) [this only applies for nodes from added] 67 | new_trees = [] 68 | 69 | if not node.is_binary: 70 | content = node.content.encode(ENCODING) 71 | else: 72 | content = node.content 73 | blob = objects.Blob.from_string(content) 74 | 75 | node_path = node.name.encode(ENCODING) 76 | if dirnames: 77 | # If there are trees which should be created we need to build 78 | # them now (in reverse order) 79 | reversed_dirnames = list(reversed(dirnames)) 80 | curtree = objects.Tree() 81 | curtree[node_path] = node.mode, blob.id 82 | new_trees.append(curtree) 83 | for dirname in reversed_dirnames[:-1]: 84 | newtree = objects.Tree() 85 | #newtree.add(DIRMOD, dirname, curtree.id) 86 | newtree[dirname] = DIRMOD, curtree.id 87 | new_trees.append(newtree) 88 | curtree = newtree 89 | parent[reversed_dirnames[-1]] = DIRMOD, curtree.id 90 | else: 91 | parent.add(name=node_path, mode=node.mode, hexsha=blob.id) 92 | 93 | new_trees.append(parent) 94 | # Update ancestors 95 | for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in 96 | zip(ancestors, ancestors[1:])]): 97 | parent[path] = DIRMOD, tree.id 98 | object_store.add_object(tree) 99 | 100 | object_store.add_object(blob) 101 | for tree in new_trees: 102 | object_store.add_object(tree) 103 | for node in self.removed: 104 | paths = node.path.split('/') 105 | tree = commit_tree 106 | trees = [tree] 107 | # Traverse deep into the forest... 108 | for path in paths: 109 | try: 110 | obj = self.repository._repo[tree[path][1]] 111 | if isinstance(obj, objects.Tree): 112 | trees.append(obj) 113 | tree = obj 114 | except KeyError: 115 | break 116 | # Cut down the blob and all rotten trees on the way back... 117 | for path, tree in reversed(zip(paths, trees)): 118 | del tree[path] 119 | if tree: 120 | # This tree still has elements - don't remove it or any 121 | # of it's parents 122 | break 123 | 124 | object_store.add_object(commit_tree) 125 | 126 | # Create commit 127 | commit = objects.Commit() 128 | commit.tree = commit_tree.id 129 | commit.parents = [p._commit.id for p in self.parents if p] 130 | commit.author = commit.committer = safe_str(author) 131 | commit.encoding = ENCODING 132 | commit.message = safe_str(message) 133 | 134 | # Compute date 135 | if date is None: 136 | date = time.time() 137 | elif isinstance(date, datetime.datetime): 138 | date = time.mktime(date.timetuple()) 139 | 140 | author_time = kwargs.pop('author_time', date) 141 | commit.commit_time = int(date) 142 | commit.author_time = int(author_time) 143 | tz = time.timezone 144 | author_tz = kwargs.pop('author_timezone', tz) 145 | commit.commit_timezone = tz 146 | commit.author_timezone = author_tz 147 | 148 | object_store.add_object(commit) 149 | 150 | ref = 'refs/heads/%s' % branch 151 | repo.refs[ref] = commit.id 152 | 153 | # Update vcs repository object & recreate dulwich repo 154 | self.repository.revisions.append(commit.id) 155 | # invalidate parsed refs after commit 156 | self.repository._parsed_refs = self.repository._get_parsed_refs() 157 | tip = self.repository.get_changeset() 158 | self.reset() 159 | return tip 160 | 161 | def _get_missing_trees(self, path, root_tree): 162 | """ 163 | Creates missing ``Tree`` objects for the given path. 164 | 165 | :param path: path given as a string. It may be a path to a file node 166 | (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must 167 | end with slash (i.e. ``foo/bar/``). 168 | :param root_tree: ``dulwich.objects.Tree`` object from which we start 169 | traversing (should be commit's root tree) 170 | """ 171 | dirpath = posixpath.split(path)[0] 172 | dirs = dirpath.split('/') 173 | if not dirs or dirs == ['']: 174 | return [] 175 | 176 | def get_tree_for_dir(tree, dirname): 177 | for name, mode, id in tree.iteritems(): 178 | if name == dirname: 179 | obj = self.repository._repo[id] 180 | if isinstance(obj, objects.Tree): 181 | return obj 182 | else: 183 | raise RepositoryError("Cannot create directory %s " 184 | "at tree %s as path is occupied and is not a " 185 | "Tree" % (dirname, tree)) 186 | return None 187 | 188 | trees = [] 189 | parent = root_tree 190 | for dirname in dirs: 191 | tree = get_tree_for_dir(parent, dirname) 192 | if tree is None: 193 | tree = objects.Tree() 194 | dirmode = 040000 195 | parent.add(dirmode, dirname, tree.id) 196 | parent = tree 197 | # Always append tree 198 | trees.append(tree) 199 | return trees 200 | -------------------------------------------------------------------------------- /vcs/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import datetime 4 | import logging 5 | 6 | import mock 7 | import sys 8 | import subprocess 9 | import vcs 10 | import vcs.cli 11 | from contextlib import nested 12 | from StringIO import StringIO 13 | from vcs.cli import BaseCommand 14 | from vcs.cli import ExecutionManager 15 | from vcs.cli import make_option 16 | from vcs.nodes import FileNode 17 | from vcs.tests.base import BackendTestMixin 18 | from vcs.tests.conf import SCM_TESTS 19 | from vcs.utils.compat import unittest 20 | 21 | 22 | class DummyExecutionManager(ExecutionManager): 23 | 24 | def get_vcsrc(self): 25 | return None 26 | 27 | 28 | class TestExecutionManager(unittest.TestCase): 29 | 30 | def test_default_argv(self): 31 | with mock.patch.object(sys, 'argv', ['vcs', 'foo', 'bar']): 32 | manager = DummyExecutionManager() 33 | self.assertEqual(manager.argv, ['foo', 'bar']) 34 | 35 | def test_default_prog_name(self): 36 | with mock.patch.object(sys, 'argv', ['vcs', 'foo', 'bar']): 37 | manager = DummyExecutionManager() 38 | self.assertEqual(manager.prog_name, 'vcs') 39 | 40 | def test_default_stdout(self): 41 | stream = StringIO() 42 | with mock.patch.object(sys, 'stdout', stream): 43 | manager = DummyExecutionManager() 44 | self.assertEqual(manager.stdout, stream) 45 | 46 | def test_default_stderr(self): 47 | stream = StringIO() 48 | with mock.patch.object(sys, 'stderr', stream): 49 | manager = DummyExecutionManager() 50 | self.assertEqual(manager.stderr, stream) 51 | 52 | def test_get_vcsrc(self): 53 | with nested(mock.patch('vcs.conf.settings.VCSRC_PATH', 'foobar'), 54 | mock.patch('vcs.cli.create_module')) as (VP, m): 55 | # Use not-dummy manager here as we need to test get_vcsrc behavior 56 | m.return_value = mock.Mock() 57 | manager = ExecutionManager() 58 | self.assertEqual(manager.vimrc, m.return_value) 59 | m.assert_called_once_with('vcsrc', 'foobar') 60 | 61 | def test_get_command_class(self): 62 | with mock.patch.object(vcs.cli, 'registry', { 63 | 'foo': 'decimal.Decimal', 64 | 'bar': 'socket.socket'}): 65 | manager = DummyExecutionManager() 66 | from decimal import Decimal 67 | from socket import socket 68 | self.assertEqual(manager.get_command_class('foo'), Decimal) 69 | self.assertEqual(manager.get_command_class('bar'), socket) 70 | 71 | def test_get_commands(self): 72 | with mock.patch.object(vcs.cli, 'registry', { 73 | 'mock': mock, 74 | 'foo': 'vcs.cli.BaseCommand', 75 | 'bar': 'vcs.tests.test_cli.DummyExecutionManager'}): 76 | manager = DummyExecutionManager() 77 | 78 | self.assertItemsEqual(manager.get_commands(), { 79 | 'foo': BaseCommand, 80 | 'bar': DummyExecutionManager, 81 | 'mock': mock, 82 | }) 83 | 84 | def test_run_command(self): 85 | manager = DummyExecutionManager(stdout=StringIO(), 86 | stderr=StringIO()) 87 | 88 | class Command(BaseCommand): 89 | 90 | def run_from_argv(self, argv): 91 | self.stdout.write(u'foo') 92 | self.stderr.write(u'bar') 93 | 94 | with mock.patch.object(manager, 'get_command_class') as m: 95 | m.return_value = Command 96 | manager.run_command('cmd', manager.argv) 97 | self.assertEqual(manager.stdout.getvalue(), u'foo') 98 | self.assertEqual(manager.stderr.getvalue(), u'bar') 99 | 100 | def test_execute_calls_run_command_if_argv_given(self): 101 | manager = DummyExecutionManager(argv=['vcs', 'show', '-h']) 102 | manager.run_command = mock.Mock() 103 | manager.execute() 104 | # we also check argv passed to the command 105 | manager.run_command.assert_called_once_with('show', 106 | ['vcs', 'show', '-h']) 107 | 108 | def test_execute_calls_show_help_if_argv_not_given(self): 109 | manager = DummyExecutionManager(argv=['vcs']) 110 | manager.show_help = mock.Mock() 111 | manager.execute() 112 | manager.show_help.assert_called_once_with() 113 | 114 | def test_show_help_writes_to_stdout(self): 115 | manager = DummyExecutionManager(stdout=StringIO(), stderr=StringIO()) 116 | manager.show_help() 117 | self.assertGreater(len(manager.stdout.getvalue()), 0) 118 | 119 | 120 | class TestBaseCommand(unittest.TestCase): 121 | 122 | def test_default_stdout(self): 123 | stream = StringIO() 124 | with mock.patch.object(sys, 'stdout', stream): 125 | command = BaseCommand() 126 | command.stdout.write(u'foobar') 127 | self.assertEqual(sys.stdout.getvalue(), u'foobar') 128 | 129 | def test_default_stderr(self): 130 | stream = StringIO() 131 | with mock.patch.object(sys, 'stderr', stream): 132 | command = BaseCommand() 133 | command.stderr.write(u'foobar') 134 | self.assertEqual(sys.stderr.getvalue(), u'foobar') 135 | 136 | def test_get_version(self): 137 | command = BaseCommand() 138 | self.assertEqual(command.get_version(), vcs.get_version()) 139 | 140 | def test_usage(self): 141 | command = BaseCommand() 142 | command.args = 'foo' 143 | self.assertEqual(command.usage('bar'), 144 | '%prog bar [options] foo') 145 | 146 | def test_get_parser(self): 147 | 148 | class Command(BaseCommand): 149 | option_list = ( 150 | make_option('-f', '--foo', action='store', dest='foo', 151 | default='bar'), 152 | ) 153 | command = Command() 154 | parser = command.get_parser('vcs', 'cmd') 155 | options, args = parser.parse_args(['-f', 'FOOBAR', 'arg1', 'arg2']) 156 | self.assertEqual(options.__dict__['foo'], 'FOOBAR') 157 | self.assertEqual(args, ['arg1', 'arg2']) 158 | 159 | def test_execute(self): 160 | command = BaseCommand() 161 | command.handle = mock.Mock() 162 | args = ['bar'] 163 | kwargs = {'debug': True} 164 | command.execute(*args, **kwargs) 165 | command.handle.assert_called_once_with(*args, **kwargs) 166 | 167 | def test_run_from_argv(self): 168 | command = BaseCommand() 169 | argv = ['vcs', 'foo', '--debug', 'bar', '--traceback'] 170 | command.handle = mock.Mock() 171 | command.run_from_argv(argv) 172 | args = ['bar'] 173 | kwargs = {'debug': True, 'traceback': True} 174 | command.handle.assert_called_once_with(*args, **kwargs) 175 | 176 | 177 | class RealCliMixin(BackendTestMixin): 178 | 179 | @classmethod 180 | def _get_commits(cls): 181 | commits = [ 182 | { 183 | 'message': u'Initial commit', 184 | 'author': u'Joe Doe ', 185 | 'date': datetime.datetime(2010, 1, 1, 20), 186 | 'added': [FileNode('file1', content='Foobar')], 187 | }, 188 | { 189 | 'message': u'Added a file', 190 | 'author': u'Joe Doe ', 191 | 'date': datetime.datetime(2010, 1, 1, 20), 192 | 'added': [FileNode('file2', content='Foobar')], 193 | }, 194 | ] 195 | return commits 196 | 197 | @unittest.skip("does not work") 198 | def test_log_command(self): 199 | cmd = 'vcs log --template "\$message"' 200 | process = subprocess.Popen(cmd, cwd=self.repo.path, shell=True, 201 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 202 | so, se = process.communicate() 203 | logging.info('out: %s', so) 204 | logging.info('err: %s', se) 205 | self.assertEqual(process.returncode, 0) 206 | self.assertEqual(so.splitlines(), [ 207 | 'Added a file', 208 | 'Initial commit', 209 | ]) 210 | 211 | 212 | # For each backend create test case class 213 | for alias in SCM_TESTS: 214 | attrs = { 215 | 'backend_alias': alias, 216 | } 217 | cls_name = ''.join(('%s real cli' % alias).title().split()) 218 | bases = (RealCliMixin, unittest.TestCase) 219 | globals()[cls_name] = type(cls_name, bases, attrs) 220 | 221 | 222 | if __name__ == '__main__': 223 | unittest.main() 224 | -------------------------------------------------------------------------------- /vcs/utils/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utitlites aimed to help achieve mostly basic tasks. 3 | """ 4 | from __future__ import division 5 | 6 | import re 7 | import os 8 | import time 9 | import datetime 10 | from subprocess import Popen, PIPE 11 | 12 | from vcs.exceptions import VCSError 13 | from vcs.exceptions import RepositoryError 14 | from vcs.utils.paths import abspath 15 | 16 | ALIASES = ['hg', 'git'] 17 | 18 | 19 | def get_scm(path, search_up=False, explicit_alias=None): 20 | """ 21 | Returns one of alias from ``ALIASES`` (in order of precedence same as 22 | shortcuts given in ``ALIASES``) and top working dir path for the given 23 | argument. If no scm-specific directory is found or more than one scm is 24 | found at that directory, ``VCSError`` is raised. 25 | 26 | :param search_up: if set to ``True``, this function would try to 27 | move up to parent directory every time no scm is recognized for the 28 | currently checked path. Default: ``False``. 29 | :param explicit_alias: can be one of available backend aliases, when given 30 | it will return given explicit alias in repositories under more than one 31 | version control, if explicit_alias is different than found it will raise 32 | VCSError 33 | """ 34 | if not os.path.isdir(path): 35 | raise VCSError("Given path %s is not a directory" % path) 36 | 37 | def get_scms(path): 38 | return [(scm, path) for scm in get_scms_for_path(path)] 39 | 40 | found_scms = get_scms(path) 41 | while not found_scms and search_up: 42 | newpath = abspath(path, '..') 43 | if newpath == path: 44 | break 45 | path = newpath 46 | found_scms = get_scms(path) 47 | 48 | if len(found_scms) > 1: 49 | for scm in found_scms: 50 | if scm[0] == explicit_alias: 51 | return scm 52 | raise VCSError('More than one [%s] scm found at given path %s' 53 | % (','.join((x[0] for x in found_scms)), path)) 54 | 55 | if len(found_scms) is 0: 56 | raise VCSError('No scm found at given path %s' % path) 57 | 58 | return found_scms[0] 59 | 60 | 61 | def get_scms_for_path(path): 62 | """ 63 | Returns all scm's found at the given path. If no scm is recognized 64 | - empty list is returned. 65 | 66 | :param path: path to directory which should be checked. May be callable. 67 | 68 | :raises VCSError: if given ``path`` is not a directory 69 | """ 70 | from vcs.backends import get_backend 71 | if hasattr(path, '__call__'): 72 | path = path() 73 | if not os.path.isdir(path): 74 | raise VCSError("Given path %r is not a directory" % path) 75 | 76 | result = [] 77 | for key in ALIASES: 78 | dirname = os.path.join(path, '.' + key) 79 | if os.path.isdir(dirname): 80 | result.append(key) 81 | continue 82 | # We still need to check if it's not bare repository as 83 | # bare repos don't have working directories 84 | try: 85 | get_backend(key)(path) 86 | result.append(key) 87 | continue 88 | except RepositoryError: 89 | # Wrong backend 90 | pass 91 | except VCSError: 92 | # No backend at all 93 | pass 94 | return result 95 | 96 | 97 | def get_repo_paths(path): 98 | """ 99 | Returns path's subdirectories which seems to be a repository. 100 | """ 101 | repo_paths = [] 102 | dirnames = (os.path.abspath(dirname) for dirname in os.listdir(path)) 103 | for dirname in dirnames: 104 | try: 105 | get_scm(dirname) 106 | repo_paths.append(dirname) 107 | except VCSError: 108 | pass 109 | return repo_paths 110 | 111 | 112 | def run_command(cmd, *args): 113 | """ 114 | Runs command on the system with given ``args``. 115 | """ 116 | command = ' '.join((cmd, args)) 117 | p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) 118 | stdout, stderr = p.communicate() 119 | return p.retcode, stdout, stderr 120 | 121 | 122 | def get_highlighted_code(name, code, type='terminal'): 123 | """ 124 | If pygments are available on the system 125 | then returned output is colored. Otherwise 126 | unchanged content is returned. 127 | """ 128 | import logging 129 | try: 130 | import pygments 131 | pygments 132 | except ImportError: 133 | return code 134 | from pygments import highlight 135 | from pygments.lexers import guess_lexer_for_filename, ClassNotFound 136 | from pygments.formatters import TerminalFormatter 137 | 138 | try: 139 | lexer = guess_lexer_for_filename(name, code) 140 | formatter = TerminalFormatter() 141 | content = highlight(code, lexer, formatter) 142 | except ClassNotFound: 143 | logging.debug("Couldn't guess Lexer, will not use pygments.") 144 | content = code 145 | return content 146 | 147 | 148 | def parse_changesets(text): 149 | """ 150 | Returns dictionary with *start*, *main* and *end* ids. 151 | 152 | Examples:: 153 | 154 | >>> parse_changesets('aaabbb') 155 | {'start': None, 'main': 'aaabbb', 'end': None} 156 | >>> parse_changesets('aaabbb..cccddd') 157 | {'start': 'aaabbb', 'main': None, 'end': 'cccddd'} 158 | 159 | """ 160 | text = text.strip() 161 | CID_RE = r'[a-zA-Z0-9]+' 162 | if not '..' in text: 163 | m = re.match(r'^(?P%s)$' % CID_RE, text) 164 | if m: 165 | return { 166 | 'start': None, 167 | 'main': text, 168 | 'end': None, 169 | } 170 | else: 171 | RE = r'^(?P%s)?\.{2,3}(?P%s)?$' % (CID_RE, CID_RE) 172 | m = re.match(RE, text) 173 | if m: 174 | result = m.groupdict() 175 | result['main'] = None 176 | return result 177 | raise ValueError("IDs not recognized") 178 | 179 | 180 | def parse_datetime(text): 181 | """ 182 | Parses given text and returns ``datetime.datetime`` instance or raises 183 | ``ValueError``. 184 | 185 | :param text: string of desired date/datetime or something more verbose, 186 | like *yesterday*, *2weeks 3days*, etc. 187 | """ 188 | 189 | text = text.strip().lower() 190 | 191 | INPUT_FORMATS = ( 192 | '%Y-%m-%d %H:%M:%S', 193 | '%Y-%m-%d %H:%M', 194 | '%Y-%m-%d', 195 | '%m/%d/%Y %H:%M:%S', 196 | '%m/%d/%Y %H:%M', 197 | '%m/%d/%Y', 198 | '%m/%d/%y %H:%M:%S', 199 | '%m/%d/%y %H:%M', 200 | '%m/%d/%y', 201 | ) 202 | for format in INPUT_FORMATS: 203 | try: 204 | return datetime.datetime(*time.strptime(text, format)[:6]) 205 | except ValueError: 206 | pass 207 | 208 | # Try descriptive texts 209 | if text == 'tomorrow': 210 | future = datetime.datetime.now() + datetime.timedelta(days=1) 211 | args = future.timetuple()[:3] + (23, 59, 59) 212 | return datetime.datetime(*args) 213 | elif text == 'today': 214 | return datetime.datetime(*datetime.datetime.today().timetuple()[:3]) 215 | elif text == 'now': 216 | return datetime.datetime.now() 217 | elif text == 'yesterday': 218 | past = datetime.datetime.now() - datetime.timedelta(days=1) 219 | return datetime.datetime(*past.timetuple()[:3]) 220 | else: 221 | days = 0 222 | matched = re.match( 223 | r'^((?P\d+) ?w(eeks?)?)? ?((?P\d+) ?d(ays?)?)?$', text) 224 | if matched: 225 | groupdict = matched.groupdict() 226 | if groupdict['days']: 227 | days += int(matched.groupdict()['days']) 228 | if groupdict['weeks']: 229 | days += int(matched.groupdict()['weeks']) * 7 230 | past = datetime.datetime.now() - datetime.timedelta(days=days) 231 | return datetime.datetime(*past.timetuple()[:3]) 232 | 233 | raise ValueError('Wrong date: "%s"' % text) 234 | 235 | 236 | def get_dict_for_attrs(obj, attrs): 237 | """ 238 | Returns dictionary for each attribute from given ``obj``. 239 | """ 240 | data = {} 241 | for attr in attrs: 242 | data[attr] = getattr(obj, attr) 243 | return data 244 | 245 | 246 | def get_total_seconds(timedelta): 247 | """ 248 | Backported for Python 2.5. 249 | 250 | See http://docs.python.org/library/datetime.html. 251 | """ 252 | return ((timedelta.microseconds + ( 253 | timedelta.seconds + 254 | timedelta.days * 24 * 60 * 60 255 | ) * 10**6) / 10**6) 256 | -------------------------------------------------------------------------------- /vcs/tests/test_utils_progressbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import sys 4 | import datetime 5 | from StringIO import StringIO 6 | from vcs.utils.helpers import get_total_seconds 7 | from vcs.utils.progressbar import AlreadyFinishedError 8 | from vcs.utils.progressbar import ProgressBar 9 | from vcs.utils.compat import unittest 10 | 11 | 12 | class TestProgressBar(unittest.TestCase): 13 | 14 | def test_default_get_separator(self): 15 | bar = ProgressBar() 16 | bar.separator = '\t' 17 | self.assertEquals(bar.get_separator(), '\t') 18 | 19 | def test_cast_to_str(self): 20 | bar = ProgressBar() 21 | self.assertEquals(str(bar), bar.get_line()) 22 | 23 | def test_default_get_bar_char(self): 24 | bar = ProgressBar() 25 | bar.bar_char = '#' 26 | self.assertEquals(bar.get_bar_char(), '#') 27 | 28 | def test_default_get_elements(self): 29 | bar = ProgressBar(elements=['foo', 'bar']) 30 | self.assertItemsEqual(bar.get_elements(), ['foo', 'bar']) 31 | 32 | def test_get_template(self): 33 | bar = ProgressBar() 34 | bar.elements = ['foo', 'bar'] 35 | bar.separator = ' ' 36 | self.assertEquals(bar.get_template().template, '$foo $bar') 37 | 38 | def test_default_stream_is_sys_stderr(self): 39 | bar = ProgressBar() 40 | self.assertEquals(bar.stream, sys.stderr) 41 | 42 | def test_get_percentage(self): 43 | bar = ProgressBar() 44 | bar.steps = 120 45 | bar.step = 60 46 | self.assertEquals(bar.get_percentage(), 50.0) 47 | bar.steps = 100 48 | bar.step = 9 49 | self.assertEquals(bar.get_percentage(), 9.0) 50 | 51 | def test_get_rendered_percentage(self): 52 | bar = ProgressBar() 53 | bar.steps = 100 54 | bar.step = 10.5 55 | self.assertEquals(bar.get_percentage(), 10.5) 56 | 57 | def test_bar_width(self): 58 | bar = ProgressBar() 59 | bar.width = 30 60 | self.assertEquals(len(bar.get_bar()), 30) 61 | 62 | def test_write(self): 63 | stream = StringIO() 64 | bar = ProgressBar() 65 | bar.stream = stream 66 | bar.write('foobar') 67 | self.assertEquals(stream.getvalue(), 'foobar') 68 | 69 | def test_change_stream(self): 70 | stream1 = StringIO() 71 | stream2 = StringIO() 72 | bar = ProgressBar() 73 | bar.stream = stream1 74 | bar.write('foo') 75 | bar.stream = stream2 76 | bar.write('bar') 77 | self.assertEquals(stream2.getvalue(), 'bar') 78 | 79 | def test_render_writes_new_line_at_last_step(self): 80 | bar = ProgressBar() 81 | bar.stream = StringIO() 82 | bar.steps = 5 83 | bar.render(5) 84 | self.assertEquals(bar.stream.getvalue()[-1], '\n') 85 | 86 | def test_initial_step_is_zero(self): 87 | bar = ProgressBar() 88 | self.assertEquals(bar.step, 0) 89 | 90 | def test_iter_starts_from_current_step(self): 91 | bar = ProgressBar() 92 | bar.stream = StringIO() 93 | bar.steps = 20 94 | bar.step = 5 95 | stepped = list(bar) 96 | self.assertEquals(stepped[0], 5) 97 | 98 | def test_iter_ends_at_last_step(self): 99 | bar = ProgressBar() 100 | bar.stream = StringIO() 101 | bar.steps = 20 102 | bar.step = 5 103 | stepped = list(bar) 104 | self.assertEquals(stepped[-1], 20) 105 | 106 | def test_get_total_time(self): 107 | bar = ProgressBar() 108 | now = datetime.datetime.now() 109 | bar.started = now - datetime.timedelta(days=1) 110 | self.assertEqual(bar.get_total_time(now), datetime.timedelta(days=1)) 111 | 112 | def test_get_total_time_returns_empty_timedelta_if_not_yet_started(self): 113 | bar = ProgressBar() 114 | self.assertEquals(bar.get_total_time(), datetime.timedelta()) 115 | 116 | def test_get_render_total_time(self): 117 | p = ProgressBar() 118 | p.time_label = 'FOOBAR' 119 | self.assertTrue(p.get_rendered_total_time().startswith('FOOBAR')) 120 | 121 | def test_get_eta(self): 122 | bar = ProgressBar(100) 123 | bar.stream = StringIO() 124 | 125 | bar.render(50) 126 | now = datetime.datetime.now() 127 | delta = now - bar.started 128 | self.assertEquals(get_total_seconds(bar.get_eta(now)), 129 | int(get_total_seconds(delta) * 0.5)) 130 | 131 | bar.render(75) 132 | now = datetime.datetime.now() 133 | delta = now - bar.started 134 | self.assertEquals(get_total_seconds(bar.get_eta(now)), 135 | int(get_total_seconds(delta) * 0.25)) 136 | 137 | def test_get_rendered_eta(self): 138 | bar = ProgressBar(100) 139 | bar.eta_label = 'foobar' 140 | self.assertTrue(bar.get_rendered_eta().startswith('foobar')) 141 | 142 | def test_get_rendered_steps(self): 143 | bar = ProgressBar(100) 144 | bar.steps_label = 'foobar' 145 | self.assertTrue(bar.get_rendered_steps().startswith('foobar')) 146 | 147 | def test_get_rendered_speed_respects_speed_label(self): 148 | bar = ProgressBar(100) 149 | bar.speed_label = 'foobar' 150 | self.assertTrue(bar.get_rendered_speed().startswith('foobar')) 151 | 152 | def test_get_rendered_speed(self): 153 | B = 1 154 | KB = B * 1024 155 | MB = KB * 1024 156 | GB = MB * 1024 157 | 158 | bar = ProgressBar(KB) 159 | self.assertEqual(bar.get_rendered_speed(512, 1), 'Speed: 512 B/s') 160 | self.assertEqual(bar.get_rendered_speed(512, 2), 'Speed: 256 B/s') 161 | self.assertEqual(bar.get_rendered_speed(900, 3), 'Speed: 300 B/s') 162 | 163 | bar = ProgressBar(GB * 10) 164 | self.assertEqual(bar.get_rendered_speed(KB, 1), 'Speed: 1 KB/s') 165 | self.assertEqual(bar.get_rendered_speed(MB, 1), 'Speed: 1.0 MB/s') 166 | self.assertEqual(bar.get_rendered_speed(GB * 4, 2), 'Speed: 2.00 GB/s') 167 | self.assertEqual(bar.get_rendered_speed(GB * 5, 2), 'Speed: 2.50 GB/s') 168 | 169 | def test_get_rendered_transfer_respects_transfer_label(self): 170 | bar = ProgressBar(100) 171 | bar.transfer_label = 'foobar' 172 | self.assertTrue(bar.get_rendered_transfer(0).startswith('foobar')) 173 | self.assertTrue(bar.get_rendered_transfer(10).startswith('foobar')) 174 | 175 | def test_get_rendered_transfer(self): 176 | B = 1 177 | KB = B * 1024 178 | MB = KB * 1024 179 | GB = MB * 1024 180 | 181 | bar = ProgressBar() 182 | self.assertEqual(bar.get_rendered_transfer(12, 100), 183 | 'Transfer: 12 B / 100 B') 184 | self.assertEqual(bar.get_rendered_transfer(KB * 5, MB), 185 | 'Transfer: 5 KB / 1.0 MB') 186 | self.assertEqual(bar.get_rendered_transfer(GB * 2.3, GB * 10), 187 | 'Transfer: 2.30 GB / 10.00 GB') 188 | 189 | 190 | def test_context(self): 191 | bar = ProgressBar() 192 | context = bar.get_context() 193 | self.assertItemsEqual(context, [ 194 | 'bar', 195 | 'percentage', 196 | 'time', 197 | 'eta', 198 | 'steps', 199 | 'speed', 200 | 'transfer', 201 | ]) 202 | 203 | def test_context_has_correct_bar(self): 204 | bar = ProgressBar() 205 | context = bar.get_context() 206 | self.assertEquals(context['bar'], bar.get_bar()) 207 | 208 | def test_context_has_correct_percentage(self): 209 | bar = ProgressBar(100) 210 | bar.step = 50 211 | percentage = bar.get_context()['percentage'] 212 | self.assertEquals(percentage, bar.get_rendered_percentage()) 213 | 214 | def test_context_has_correct_total_time(self): 215 | bar = ProgressBar(100) 216 | time = bar.get_context()['time'] 217 | self.assertEquals(time, bar.get_rendered_total_time()) 218 | 219 | def test_context_has_correct_eta(self): 220 | bar = ProgressBar(100) 221 | eta = bar.get_context()['eta'] 222 | self.assertEquals(eta, bar.get_rendered_eta()) 223 | 224 | def test_context_has_correct_steps(self): 225 | bar = ProgressBar(100) 226 | steps = bar.get_context()['steps'] 227 | self.assertEquals(steps, bar.get_rendered_steps()) 228 | 229 | def context_has_correct_speed(self): 230 | bar = ProgressBar(100) 231 | speed = bar.get_context()['speed'] 232 | self.assertEquals(speed, bar.get_rendered_speed()) 233 | 234 | def test_render_raises_error_if_bar_already_finished(self): 235 | bar = ProgressBar(10) 236 | bar.stream = StringIO() 237 | bar.render(10) 238 | 239 | with self.assertRaises(AlreadyFinishedError): 240 | bar.render(0) 241 | 242 | 243 | if __name__ == '__main__': 244 | unittest.main() 245 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | Say you don't want to install ``vcs`` or just want to begin with really fast 7 | tutorial? Not a problem, just follow sections below. 8 | 9 | Prepare 10 | ------- 11 | 12 | We will try to show you how you can use ``vcs`` directly on repository. But 13 | hey, ``vcs`` is maintained within git `repository 14 | `_ already, so why not use it? Simply run 15 | following commands in your shell 16 | 17 | .. code-block:: bash 18 | 19 | cd /tmp 20 | git clone git://github.com/codeinn/vcs.git 21 | cd vcs 22 | 23 | Now run your python interpreter of choice:: 24 | 25 | $ python 26 | >>> 27 | 28 | .. note:: 29 | You may of course put your clone of ``vcs`` wherever you like but running 30 | python shell *inside* of it would allow you to use just cloned version of 31 | ``vcs``. 32 | 33 | Take the shortcut 34 | ----------------- 35 | 36 | There is no need to import everything from ``vcs`` - in fact, all you'd need is 37 | to import ``get_repo``, at least for now. Then, simply initialize repository 38 | object by providing it's type and path. 39 | 40 | .. code-block:: python 41 | 42 | >>> import vcs 43 | >>> # create repository representation at current dir 44 | >>> repo = vcs.get_repo(path='') 45 | 46 | .. note:: 47 | In above example we didn't specify scm. We can provide as second argument to 48 | the ``get_repo`` function, i.e. ``get_repo('', 'hg')``. 49 | 50 | Basics 51 | ------ 52 | 53 | Let's ask repo about the content... 54 | 55 | .. code-block:: python 56 | 57 | >>> root = repo.get_changeset().get_node('') 58 | >>> print root.nodes # prints nodes of the RootNode 59 | [, , , # ... (chopped) 60 | >>> 61 | >>> # get 10th changeset 62 | >>> chset = repo.get_changeset(10) 63 | >>> print chset 64 | 65 | >>> 66 | >>> # any backend would return latest changeset if revision is not given 67 | >>> tip = repo.get_changeset() 68 | >>> tip == repo.get_changeset('tip') # for git/mercurial backend 'tip' is allowed 69 | True 70 | >>> tip == repo.get_changeset(None) # any backend allow revision to be None (default) 71 | True 72 | >>> tip.raw_id == repo.revisions[-1] 73 | True 74 | >>> 75 | >>> # Iterate repository 76 | >>> for cs in repo: 77 | ... print cs 78 | ... 79 | ... 80 | >>> 81 | >>> 82 | >>> ... 83 | 84 | Walking 85 | ------- 86 | 87 | Now let's ask for nodes at revision faebbb751cc36c137127c50f57bcdb5f1c540013 88 | (https://github.com/codeinn/vcs/commit/faebbb751cc36c137127c50f57bcdb5f1c540013) 89 | 90 | .. code-block:: python 91 | 92 | >>> chset = repo.get_changeset('faebbb751cc36c137127c50f57bcdb5f1c540013') 93 | >>> root = chset.root 94 | >>> print root.dirs 95 | [, , ] 96 | 97 | .. note:: 98 | 99 | :ref:`api-nodes` are objects representing files and directories within the 100 | repository revision. 101 | 102 | .. code-block:: python 103 | 104 | >>> # Fetch vcs directory 105 | >>> vcs = repo.get_changeset('faebbb751cc36c137127c50f57bcdb5f1c540013').get_node('vcs') 106 | >>> print vcs.dirs 107 | [, 108 | , 109 | ] 110 | 111 | >>> backends_node = vcs.dirs[0] 112 | >>> print backends_node.nodes 113 | [, 114 | , 115 | , 116 | ] 117 | 118 | >>> print '\n'.join(backends_node.files[0].content.splitlines()[:4]) 119 | # -*- coding: utf-8 -*- 120 | """ 121 | vcs.backends 122 | ~~~~~~~~~~~~ 123 | 124 | 125 | Getting meta data 126 | ----------------- 127 | 128 | Make ``vcs`` show us some meta information 129 | 130 | Tags and branches 131 | ~~~~~~~~~~~~~~~~~ 132 | 133 | .. code-block:: python 134 | 135 | >>> print repo.branches 136 | OrderedDict([('master', 'fe568b4081755c12abf6ba673ba777fc02a415f3')]) 137 | >>> for tag, raw_id in repo.tags.items(): 138 | ... print tag.rjust(10), '|', raw_id 139 | ... 140 | v0.1.9 | 341d28f0eec5ddf0b6b77871e13c2bbd6bec685c 141 | v0.1.8 | 74ebce002c088b8a5ecf40073db09375515ecd68 142 | v0.1.7 | 4d78bf73b5c22c82b68f902f138f7881b4fffa2c 143 | v0.1.6 | 0205cb3f44223fb3099d12a77a69c81b798772d9 144 | v0.1.5 | 6c0ce52b229aa978889e91b38777f800e85f330b 145 | v0.1.4 | 7d735150934cd7645ac3051903add952390324a5 146 | v0.1.3 | 5a3a8fb005554692b16e21dee62bf02667d8dc3e 147 | v0.1.2 | 0ba5f8a4660034ff25c0cac2a5baabf5d2791d63 148 | v0.1.11 | c60f01b77c42dce653d6b1d3b04689862c261929 149 | v0.1.10 | 10cddef6b794696066fb346434014f0a56810218 150 | v0.1.1 | e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0 151 | 152 | Give me a file, finally! 153 | ~~~~~~~~~~~~~~~~~~~~~~~~ 154 | 155 | .. code-block:: python 156 | 157 | >>> import vcs 158 | >>> repo = vcs.get_repo('') 159 | >>> chset = repo.get_changeset('faebbb751cc36c137127c50f57bcdb5f1c540013') 160 | >>> root = chset.get_node('') 161 | >>> backends = root.get_node('vcs/backends') 162 | >>> backends.files 163 | [, 164 | , 165 | , 166 | ] 167 | >>> f = backends.get_node('hg.py') 168 | >>> f.name 169 | 'hg.py' 170 | >>> f.path 171 | 'vcs/backends/hg.py' 172 | >>> f.size 173 | 28549 174 | >>> f.last_changeset 175 | 176 | >>> f.last_changeset.date 177 | datetime.datetime(2011, 2, 28, 23, 23, 5) 178 | >>> f.last_changeset.message 179 | u'fixed bug in get_changeset when 0 or None was passed' 180 | >>> f.last_changeset.author 181 | u'marcinkuzminski ' 182 | >>> f.mimetype 183 | 'text/x-python' 184 | >>> # Following would raise exception unless you have pygments installed 185 | >>> f.lexer 186 | 187 | >>> f.lexer_alias # shortcut to get first of lexers' available aliases 188 | 'python' 189 | >>> # wanna go back? why? oh, whatever... 190 | >>> f.parent 191 | 192 | >>> 193 | >>> # is it cached? Hell yeah... 194 | >>> f is f.parent.get_node('hg.py') is chset.get_node('vcs/backends/hg.py') 195 | True 196 | 197 | How about history? 198 | ~~~~~~~~~~~~~~~~~~ 199 | 200 | It is possible to retrieve changesets for which file node has been changed and 201 | this is pretty damn simple. Let's say we want to see history of the file located 202 | at ``vcs/nodes.py``. 203 | 204 | .. code-block:: python 205 | 206 | >>> f = repo.get_changeset().get_node('vcs/nodes.py') 207 | >>> for cs in f.history: 208 | ... print cs 209 | ... 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | Note that ``history`` attribute is computed lazily and returned list is reversed 266 | - changesets are retrieved from most recent to oldest. 267 | 268 | Show me the difference! 269 | ~~~~~~~~~~~~~~~~~~~~~~~ 270 | 271 | Here we present naive implementation of diff table for the given file node 272 | located at ``vcs/nodes.py``. First we have to get the node from repository. 273 | After that we retrieve last changeset for which the file has been modified 274 | and we create a html file using `difflib`_. 275 | 276 | .. code-block:: python 277 | 278 | >>> new = repo.get_changeset(repo.tags['v0.1.11']) 279 | >>> old = repo.get_changeset(repo.tags['v0.1.10']) 280 | >>> f_old = old.get_node('vcs/nodes.py') 281 | >>> f_new = new.get_node('vcs/nodes.py') 282 | >>> f_old = repo.get_changeset(81).get_node(f.path) 283 | >>> out = open('/tmp/out.html', 'w') 284 | >>> from difflib import HtmlDiff 285 | >>> hd = HtmlDiff(tabsize=4) 286 | >>> diffs = hd.make_file(f_new.content.split('\n'), f_old.content.split('\n')) 287 | >>> out.write(diffs) 288 | >>> out.close() 289 | 290 | Now open file at ``/tmp/out.html`` in your favorite browser. 291 | 292 | .. _difflib: http://docs.python.org/library/difflib.html 293 | -------------------------------------------------------------------------------- /vcs/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import os 4 | import mock 5 | import time 6 | import shutil 7 | import tempfile 8 | import datetime 9 | from vcs.utils.compat import unittest 10 | from vcs.utils.paths import get_dirs_for_path 11 | from vcs.utils.helpers import get_dict_for_attrs 12 | from vcs.utils.helpers import get_scm 13 | from vcs.utils.helpers import get_scms_for_path 14 | from vcs.utils.helpers import get_total_seconds 15 | from vcs.utils.helpers import parse_changesets 16 | from vcs.utils.helpers import parse_datetime 17 | from vcs.utils import author_email, author_name 18 | from vcs.utils.paths import get_user_home 19 | from vcs.exceptions import VCSError 20 | 21 | from vcs.tests.conf import TEST_HG_REPO, TEST_GIT_REPO, TEST_TMP_PATH 22 | 23 | 24 | class PathsTest(unittest.TestCase): 25 | 26 | def _test_get_dirs_for_path(self, path, expected): 27 | """ 28 | Tests if get_dirs_for_path returns same as expected. 29 | """ 30 | expected = sorted(expected) 31 | result = sorted(get_dirs_for_path(path)) 32 | self.assertEqual(result, expected, 33 | msg="%s != %s which was expected result for path %s" 34 | % (result, expected, path)) 35 | 36 | def test_get_dirs_for_path(self): 37 | path = 'foo/bar/baz/file' 38 | paths_and_results = ( 39 | ('foo/bar/baz/file', ['foo', 'foo/bar', 'foo/bar/baz']), 40 | ('foo/bar/', ['foo', 'foo/bar']), 41 | ('foo/bar', ['foo']), 42 | ) 43 | for path, expected in paths_and_results: 44 | self._test_get_dirs_for_path(path, expected) 45 | 46 | 47 | def test_get_scm(self): 48 | self.assertEqual(('hg', TEST_HG_REPO), get_scm(TEST_HG_REPO)) 49 | self.assertEqual(('git', TEST_GIT_REPO), get_scm(TEST_GIT_REPO)) 50 | 51 | def test_get_two_scms_for_path(self): 52 | multialias_repo_path = os.path.join(TEST_TMP_PATH, 'hg-git-repo-2') 53 | if os.path.isdir(multialias_repo_path): 54 | shutil.rmtree(multialias_repo_path) 55 | 56 | os.mkdir(multialias_repo_path) 57 | 58 | self.assertRaises(VCSError, get_scm, multialias_repo_path) 59 | 60 | def test_get_scm_error_path(self): 61 | self.assertRaises(VCSError, get_scm, 'err') 62 | 63 | def test_get_scms_for_path(self): 64 | dirpath = tempfile.gettempdir() 65 | new = os.path.join(dirpath, 'vcs-scms-for-path-%s' % time.time()) 66 | os.mkdir(new) 67 | self.assertEqual(get_scms_for_path(new), []) 68 | 69 | os.mkdir(os.path.join(new, '.tux')) 70 | self.assertEqual(get_scms_for_path(new), []) 71 | 72 | os.mkdir(os.path.join(new, '.git')) 73 | self.assertEqual(set(get_scms_for_path(new)), set(['git'])) 74 | 75 | os.mkdir(os.path.join(new, '.hg')) 76 | self.assertEqual(set(get_scms_for_path(new)), set(['git', 'hg'])) 77 | 78 | 79 | class TestParseChangesets(unittest.TestCase): 80 | 81 | def test_main_is_returned_correctly(self): 82 | self.assertEqual(parse_changesets('123456'), { 83 | 'start': None, 84 | 'main': '123456', 85 | 'end': None, 86 | }) 87 | 88 | def test_start_is_returned_correctly(self): 89 | self.assertEqual(parse_changesets('aaabbb..'), { 90 | 'start': 'aaabbb', 91 | 'main': None, 92 | 'end': None, 93 | }) 94 | 95 | def test_end_is_returned_correctly(self): 96 | self.assertEqual(parse_changesets('..cccddd'), { 97 | 'start': None, 98 | 'main': None, 99 | 'end': 'cccddd', 100 | }) 101 | 102 | def test_that_two_or_three_dots_are_allowed(self): 103 | text1 = 'a..b' 104 | text2 = 'a...b' 105 | self.assertEqual(parse_changesets(text1), parse_changesets(text2)) 106 | 107 | def test_that_input_is_stripped_first(self): 108 | text1 = 'a..bb' 109 | text2 = ' a..bb\t\n\t ' 110 | self.assertEqual(parse_changesets(text1), parse_changesets(text2)) 111 | 112 | def test_that_exception_is_raised(self): 113 | text = '123456.789012' # single dot is not recognized 114 | with self.assertRaises(ValueError): 115 | parse_changesets(text) 116 | 117 | def test_non_alphanumeric_raises_exception(self): 118 | with self.assertRaises(ValueError): 119 | parse_changesets('aaa@bbb') 120 | 121 | 122 | class TestParseDatetime(unittest.TestCase): 123 | 124 | def test_datetime_text(self): 125 | self.assertEqual(parse_datetime('2010-04-07 21:29:41'), 126 | datetime.datetime(2010, 4, 7, 21, 29, 41)) 127 | 128 | def test_no_seconds(self): 129 | self.assertEqual(parse_datetime('2010-04-07 21:29'), 130 | datetime.datetime(2010, 4, 7, 21, 29)) 131 | 132 | def test_date_only(self): 133 | self.assertEqual(parse_datetime('2010-04-07'), 134 | datetime.datetime(2010, 4, 7)) 135 | 136 | def test_another_format(self): 137 | self.assertEqual(parse_datetime('04/07/10 21:29:41'), 138 | datetime.datetime(2010, 4, 7, 21, 29, 41)) 139 | 140 | def test_now(self): 141 | self.assertTrue(parse_datetime('now') - datetime.datetime.now() < 142 | datetime.timedelta(seconds=1)) 143 | 144 | def test_today(self): 145 | today = datetime.date.today() 146 | self.assertEqual(parse_datetime('today'), 147 | datetime.datetime(*today.timetuple()[:3])) 148 | 149 | def test_yesterday(self): 150 | yesterday = datetime.date.today() - datetime.timedelta(days=1) 151 | self.assertEqual(parse_datetime('yesterday'), 152 | datetime.datetime(*yesterday.timetuple()[:3])) 153 | 154 | def test_tomorrow(self): 155 | tomorrow = datetime.date.today() + datetime.timedelta(days=1) 156 | args = tomorrow.timetuple()[:3] + (23, 59, 59) 157 | self.assertEqual(parse_datetime('tomorrow'), datetime.datetime(*args)) 158 | 159 | def test_days(self): 160 | timestamp = datetime.datetime.today() - datetime.timedelta(days=3) 161 | args = timestamp.timetuple()[:3] + (0, 0, 0, 0) 162 | expected = datetime.datetime(*args) 163 | self.assertEqual(parse_datetime('3d'), expected) 164 | self.assertEqual(parse_datetime('3 d'), expected) 165 | self.assertEqual(parse_datetime('3 day'), expected) 166 | self.assertEqual(parse_datetime('3 days'), expected) 167 | 168 | def test_weeks(self): 169 | timestamp = datetime.datetime.today() - datetime.timedelta(days=3 * 7) 170 | args = timestamp.timetuple()[:3] + (0, 0, 0, 0) 171 | expected = datetime.datetime(*args) 172 | self.assertEqual(parse_datetime('3w'), expected) 173 | self.assertEqual(parse_datetime('3 w'), expected) 174 | self.assertEqual(parse_datetime('3 week'), expected) 175 | self.assertEqual(parse_datetime('3 weeks'), expected) 176 | 177 | def test_mixed(self): 178 | timestamp = datetime.datetime.today() - datetime.timedelta(days=2 * 7 + 3) 179 | args = timestamp.timetuple()[:3] + (0, 0, 0, 0) 180 | expected = datetime.datetime(*args) 181 | self.assertEqual(parse_datetime('2w3d'), expected) 182 | self.assertEqual(parse_datetime('2w 3d'), expected) 183 | self.assertEqual(parse_datetime('2w 3 days'), expected) 184 | self.assertEqual(parse_datetime('2 weeks 3 days'), expected) 185 | 186 | 187 | class TestAuthorExtractors(unittest.TestCase): 188 | TEST_AUTHORS = [('Marcin Kuzminski ', 189 | ('Marcin Kuzminski', 'marcin@python-works.com')), 190 | ('Marcin Kuzminski Spaces < marcin@python-works.com >', 191 | ('Marcin Kuzminski Spaces', 'marcin@python-works.com')), 192 | ('Marcin Kuzminski ', 193 | ('Marcin Kuzminski', 'marcin.kuzminski@python-works.com')), 194 | ('mrf RFC_SPEC ', 195 | ('mrf RFC_SPEC', 'marcin+kuzminski@python-works.com')), 196 | ('username ', 197 | ('username', 'user@email.com')), 198 | ('username ', 203 | ('', 'justemail@mail.com')), 204 | ('justname', 205 | ('justname', '')), 206 | ('Mr Double Name withemail@email.com ', 207 | ('Mr Double Name', 'withemail@email.com')), 208 | ] 209 | 210 | def test_author_email(self): 211 | 212 | for test_str, result in self.TEST_AUTHORS: 213 | self.assertEqual(result[1], author_email(test_str)) 214 | 215 | 216 | def test_author_name(self): 217 | 218 | for test_str, result in self.TEST_AUTHORS: 219 | self.assertEqual(result[0], author_name(test_str)) 220 | 221 | 222 | class TestGetDictForAttrs(unittest.TestCase): 223 | 224 | def test_returned_dict_has_expected_attrs(self): 225 | obj = mock.Mock() 226 | obj.NOT_INCLUDED = 'this key/value should not be included' 227 | obj.CONST = True 228 | obj.foo = 'aaa' 229 | obj.attrs = {'foo': 'bar'} 230 | obj.date = datetime.datetime(2010, 12, 31) 231 | obj.count = 1001 232 | 233 | self.assertEqual(get_dict_for_attrs(obj, ['CONST', 'foo', 'attrs', 234 | 'date', 'count']), { 235 | 'CONST': True, 236 | 'foo': 'aaa', 237 | 'attrs': {'foo': 'bar'}, 238 | 'date': datetime.datetime(2010, 12, 31), 239 | 'count': 1001, 240 | }) 241 | 242 | 243 | class TestGetTotalSeconds(unittest.TestCase): 244 | 245 | def assertTotalSecondsEqual(self, timedelta, expected_seconds): 246 | result = get_total_seconds(timedelta) 247 | self.assertEqual(result, expected_seconds, 248 | "We computed %s seconds for %s but expected %s" 249 | % (result, timedelta, expected_seconds)) 250 | 251 | def test_get_total_seconds_returns_proper_value(self): 252 | self.assertTotalSecondsEqual(datetime.timedelta(seconds=1001), 1001) 253 | 254 | def test_get_total_seconds_returns_proper_value_for_partial_seconds(self): 255 | self.assertTotalSecondsEqual(datetime.timedelta(seconds=50.65), 50.65) 256 | 257 | 258 | class TestGetUserHome(unittest.TestCase): 259 | 260 | @mock.patch.object(os, 'environ', {}) 261 | def test_defaults_to_none(self): 262 | self.assertEqual(get_user_home(), '') 263 | 264 | @mock.patch.object(os, 'environ', {'HOME': '/home/foobar'}) 265 | def test_unix_like(self): 266 | self.assertEqual(get_user_home(), '/home/foobar') 267 | 268 | @mock.patch.object(os, 'environ', {'USERPROFILE': '/Users/foobar'}) 269 | def test_windows_like(self): 270 | self.assertEqual(get_user_home(), '/Users/foobar') 271 | 272 | @mock.patch.object(os, 'environ', {'HOME': '/home/foobar', 273 | 'USERPROFILE': '/Users/foobar'}) 274 | def test_prefers_home_over_userprofile(self): 275 | self.assertEqual(get_user_home(), '/home/foobar') 276 | 277 | 278 | if __name__ == '__main__': 279 | unittest.main() 280 | --------------------------------------------------------------------------------