pax_global_header00006660000000000000000000000064141041340070014504gustar00rootroot0000000000000052 comment=c20d4b48df4e5c36976cab19d00577a19649027c vcstool-0.3.0/000077500000000000000000000000001410413400700131755ustar00rootroot00000000000000vcstool-0.3.0/.github/000077500000000000000000000000001410413400700145355ustar00rootroot00000000000000vcstool-0.3.0/.github/workflows/000077500000000000000000000000001410413400700165725ustar00rootroot00000000000000vcstool-0.3.0/.github/workflows/ci.yml000066400000000000000000000023041410413400700177070ustar00rootroot00000000000000name: vcstool on: push: pull_request: types: [opened, synchronize, reopened] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] python-version: [3.5, 3.6, 3.7, 3.8] include: - os: macos-latest python-version: 3.8 - os: windows-latest python-version: 3.8 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade PyYAML - name: Install dependencies (macOS) run: | brew install subversion mercurial if: matrix.os == 'macos-latest' - name: Test with pytest run: | pip install --upgrade coverage flake8 flake8-docstrings flake8-import-order pytest git config --global --add init.defaultBranch master git config --global --add advice.detachedHead true ${{ matrix.os == 'windows-latest' && 'set PYTHONPATH=%cd% &&' || 'PYTHONPATH=`pwd`' }} pytest -s -v test vcstool-0.3.0/.gitignore000066400000000000000000000000531410413400700151630ustar00rootroot00000000000000*.pyc build deb_dist dist vcstool.egg-info vcstool-0.3.0/CONTRIBUTING.md000066400000000000000000000011711410413400700154260ustar00rootroot00000000000000Any contribution that you make to this repository will be under the Apache 2 License, as dictated by that [license](http://www.apache.org/licenses/LICENSE-2.0.html): ~~~ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. ~~~ vcstool-0.3.0/LICENSE000066400000000000000000000261231410413400700142060ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2012-2015 Dirk Thomas Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. vcstool-0.3.0/MANIFEST.in000066400000000000000000000002471410413400700147360ustar00rootroot00000000000000include vcstool-completion/vcs.bash vcstool-completion/vcs.tcsh vcstool-completion/vcs.zsh include vcstool-completion/vcs.fish include test/*.repos include test/*.txt vcstool-0.3.0/Makefile000066400000000000000000000006001410413400700146310ustar00rootroot00000000000000.PHONY: all setup clean_dist distro clean install NAME=vcstool VERSION=`./setup.py --version` all: echo "noop for debbuild" setup: echo "building version ${VERSION}" clean_dist: -rm -rf deb_dist -rm -rf dist -rm -rf vcstool.egg-info distro: setup clean_dist python setup.py sdist clean: clean_dist echo "clean" install: distro sudo checkinstall python setup.py install vcstool-0.3.0/README.rst000066400000000000000000000221651410413400700146720ustar00rootroot00000000000000What is vcstool? ================ Vcstool is a version control system (VCS) tool, designed to make working with multiple repositories easier. Note: This tool should not be confused with `vcstools `_ (with a trailing ``s``) which provides a Python API for interacting with different version control systems. The biggest differences between the two are: * ``vcstool`` doesn't use any state beside the repository working copies available in the filesystem. * The file format of ``vcstool export`` uses the relative paths of the repositories as keys in YAML which avoids collisions by design. * ``vcstool`` has significantly fewer lines of code than ``vcstools`` including the command line tools built on top. Python 2.7 / <= 3.4 support --------------------------- The latest version supporting Python 2.7 and Python <= 3.4 is 0.2.x from the `0.2.x branch `_. How does it work? ----------------- Vcstool operates on any folder from where it recursively searches for supported repositories. On these repositories vcstool invokes the native VCS client with the requested command (i.e. *diff*). Which VCS types are supported? ------------------------------ Vcstool supports `Git `_, `Mercurial `_, `Subversion `_, `Bazaar `_. How to use vcstool? ------------------- The script ``vcs`` can be used similarly to the VCS clients ``git``, ``hg`` etc. The ``help`` command provides a list of available commands with an additional description:: vcs help By default vcstool searches for repositories under the current folder. Optionally one path (or multiple paths) can be passed to search for repositories at different locations:: vcs status /path/to/several/repos /path/to/other/repos /path/to/single/repo Exporting and importing sets of repositories -------------------------------------------- Vcstool can export and import all the information required to reproduce the versions of a set of repositories. Vcstool uses a simple `YAML `_ format to encode this information. This format includes a root key ``repositories`` under which each local repository is described by a dictionary keyed by its relative path. Each of these dictionaries contains keys ``type``, ``url``, and ``version``. If the ``version`` key is omitted the default branch is being used. This results in something similar to the following for a set of two repositories (`vcstool `_ cloned via Git and `rosinstall `_ checked out via Subversion): .. code-block:: yaml repositories: vcstool: type: git url: git@github.com:dirk-thomas/vcstool.git version: master old_tools/rosinstall: type: svn url: https://github.com/vcstools/rosinstall/trunk version: 748 Export set of repositories ~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``vcs export`` command outputs the path, vcs type, URL and version information for all repositories in `YAML `_ format. The output is usually piped to a file:: vcs export > my.repos If the repository is currently on the tip of a branch the branch is followed. This implies that a later import might fetch a newer revision if the branch has evolved in the meantime. Furthermore if the local branch has evolved from the remote repository an import might not result in the exact same state. To make sure to store the exact revision in the exported data use the command line argument ``--exact``. Since a specific revision is not tied to neither a branch nor a remote (for Git and Mercurial) the tool will check if the current hash exists in any of the remotes. If it exists in multiple the remotes ``origin`` and ``upstream`` are considered before any other in alphabetical order. Import set of repositories ~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``vcs import`` command clones all repositories which are passed in via ``stdin`` in YAML format. Usually the data of a previously exported file is piped in:: vcs import < my.repos The ``import`` command also supports input in the `rosinstall file format `_. Beside passing a file path the command also supports passing a URL. Only for this command vcstool supports the pseudo clients ``tar`` and ``zip`` which fetch a tarball / zipfile from a URL and unpack its content. For those two types the ``version`` key is optional. If specified only entries from the archive which are in the subfolder specified by the version value are being extracted. Validate repositories file ~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``vcs validate`` command takes a YAML file which is passed in via ``stdin`` and validates its contents and format. The data of a previously-exported file or hand-generated file are piped in:: vcs validate < my.repos The ``validate`` command also supports input in the `rosinstall file format `_. Advanced features ----------------- Show log since last tag ~~~~~~~~~~~~~~~~~~~~~~~ The ``vcs log`` command supports the argument ``--limit-untagged`` which will output the log for all commits since the last tag. Parallelization and stdin ~~~~~~~~~~~~~~~~~~~~~~~~~ By default ``vcs`` parallelizes the work across multiple repositories based on the number of CPU cores. In the case that the invoked commands require input from ``stdin`` that parallelization is a problem. In order to be able to provide input to each command separately these commands must run sequentially. When needing to e.g. interactively provide credentials all commands should be executed sequentially by passing: --workers 1 In the case repositories are using SSH ``git@`` URLs but the host is not known yet ``vcs import`` automatically falls back to a single worker. Run arbitrary commands ~~~~~~~~~~~~~~~~~~~~~~ The ``vcs custom`` command enables to pass arbitrary user-specified arguments to the vcs invocation. The set of repositories to operate on can optionally be restricted by the type: vcs custom --git --args log --oneline -n 10 If the command should work on multiple repositories make sure to pass only generic arguments which work for all of these repository types. How to install vcstool? ======================= On Debian-based platforms the recommended method is to install the package *python3-vcstool*. On Ubuntu this is done using *apt-get*: If you are using `ROS `_ you can get the package directly from the ROS repository:: sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' sudo apt install curl # if you haven't already installed curl curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - sudo apt-get update sudo apt-get install python3-vcstool If you are not using ROS or if you want the latest release as soon as possible you can get the package from |packagecloud.io|:: curl -s https://packagecloud.io/install/repositories/dirk-thomas/vcstool/script.deb.sh | sudo bash sudo apt-get update sudo apt-get install python3-vcstool .. |packagecloud.io| image:: https://img.shields.io/badge/deb-packagecloud.io-844fec.svg :target: https://packagecloud.io/dirk-thomas/vcstool :alt: packagecloud.io On other systems, use the `PyPI `_ package:: sudo pip install vcstool Setup auto-completion --------------------- For the shells *bash*, *tcsh* and *zsh* vcstool can provide auto-completion of the various VCS commands. In order to enable that feature the shell specific completion file must be sourced. For *bash* append the following line to the ``~/.bashrc`` file:: source /usr/share/vcstool-completion/vcs.bash For *tcsh* append the following line to the ``~/.cshrc`` file:: source /usr/share/vcstool-completion/vcs.tcsh For *zsh* append the following line to the ``~/.zshrc`` file:: source /usr/share/vcstool-completion/vcs.zsh For *fish* append the following line to the ``~/.config/fishconfig.fish`` file:: source /usr/share/vcstool-completion/vcs.fish How to contribute? ================== How to report problems? ----------------------- Before reporting a problem please make sure to use the latest version. Issues can be filled on `GitHub `_ after making sure that this problem has not yet been reported. Please make sure to include as much information, i.e. version numbers from vcstool, operating system, Python and a reproducible example of the commands which expose the problem. How to try the latest changes? ------------------------------ Sourcing the ``setup.sh`` file prepends the ``src`` folder to the ``PYTHONPATH`` and the ``scripts`` folder to the ``PATH``. Then vcstool can be used with the commands ``vcs-COMMAND`` (note the hyphen between ``vcs`` and ``command`` instead of a space). Alternatively the ``-e/--editable`` flag of ``pip`` can be used:: # from the top level of this repo pip3 install --user -e . vcstool-0.3.0/publish-python.yaml000066400000000000000000000005221410413400700170450ustar00rootroot00000000000000artifacts: - type: wheel uploads: - type: pypi - type: stdeb uploads: - type: packagecloud config: repository: dirk-thomas/vcstool distributions: - ubuntu:xenial - ubuntu:bionic - ubuntu:focal - debian:stretch - debian:buster vcstool-0.3.0/scripts/000077500000000000000000000000001410413400700146645ustar00rootroot00000000000000vcstool-0.3.0/scripts/vcs000077500000000000000000000001411410413400700154010ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.vcs import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-branch000077500000000000000000000001441410413400700166370ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.branch import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-bzr000077500000000000000000000001541410413400700162000ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.custom import bzr_main sys.exit(bzr_main() or 0) vcstool-0.3.0/scripts/vcs-custom000077500000000000000000000001441410413400700167140ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.custom import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-diff000077500000000000000000000001421410413400700163100ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.diff import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-export000077500000000000000000000001441410413400700167230ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.export import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-git000077500000000000000000000001541410413400700161660ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.custom import git_main sys.exit(git_main() or 0) vcstool-0.3.0/scripts/vcs-help000077500000000000000000000001421410413400700163300ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.help import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-hg000077500000000000000000000001521410413400700157770ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.custom import hg_main sys.exit(hg_main() or 0) vcstool-0.3.0/scripts/vcs-import000077500000000000000000000001451410413400700167150ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.import_ import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-log000077500000000000000000000001411410413400700161600ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.log import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-pull000077500000000000000000000001421410413400700163540ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.pull import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-push000077500000000000000000000001421410413400700163570ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.push import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-remotes000077500000000000000000000001451410413400700170610ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.remotes import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-status000077500000000000000000000001441410413400700167250ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.status import main sys.exit(main() or 0) vcstool-0.3.0/scripts/vcs-svn000077500000000000000000000001541410413400700162110ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.custom import svn_main sys.exit(svn_main() or 0) vcstool-0.3.0/scripts/vcs-validate000077500000000000000000000001461410413400700171750ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from vcstool.commands.validate import main sys.exit(main() or 0) vcstool-0.3.0/setup.py000066400000000000000000000044671410413400700147220ustar00rootroot00000000000000from setuptools import find_packages from setuptools import setup from vcstool import __version__ install_requires = ['PyYAML', 'setuptools'] setup( name='vcstool', version=__version__, install_requires=install_requires, packages=find_packages(), author='Dirk Thomas', author_email='web@dirk-thomas.net', maintainer='Dirk Thomas', maintainer_email='web@dirk-thomas.net', url='https://github.com/dirk-thomas/vcstool', download_url='http://download.ros.org/downloads/vcstool/', classifiers=['Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Topic :: Software Development :: Version Control', 'Topic :: Utilities'], description='vcstool provides a command line tool to invoke vcs commands ' 'on multiple repositories.', long_description='\ vcstool enables batch commands on multiple different vcs repositories. \ Currently it supports git, hg, svn and bzr.', license='Apache License, Version 2.0', data_files=[ ('share/vcstool-completion', [ 'vcstool-completion/vcs.bash', 'vcstool-completion/vcs.tcsh', 'vcstool-completion/vcs.zsh', 'vcstool-completion/vcs.fish' ]) ], entry_points={ 'console_scripts': [ 'vcs = vcstool.commands.vcs:main', 'vcs-branch = vcstool.commands.branch:main', 'vcs-bzr = vcstool.commands.custom:bzr_main', 'vcs-custom = vcstool.commands.custom:main', 'vcs-diff = vcstool.commands.diff:main', 'vcs-export = vcstool.commands.export:main', 'vcs-git = vcstool.commands.custom:git_main', 'vcs-help = vcstool.commands.help:main', 'vcs-hg = vcstool.commands.custom:hg_main', 'vcs-import = vcstool.commands.import_:main', 'vcs-log = vcstool.commands.log:main', 'vcs-pull = vcstool.commands.pull:main', 'vcs-push = vcstool.commands.push:main', 'vcs-remotes = vcstool.commands.remotes:main', 'vcs-status = vcstool.commands.status:main', 'vcs-svn = vcstool.commands.custom:svn_main', 'vcs-validate = vcstool.commands.validate:main', ] } ) vcstool-0.3.0/setup.sh000066400000000000000000000005631410413400700146750ustar00rootroot00000000000000if [ "$BASH_SOURCE" ]; then BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" else SCRIPT=$(readlink -f $0) BASEDIR=$(dirname $SCRIPT) if [ ! -f "$BASEDIR/setup.sh" ]; then echo "In non-bash shells the setup.sh file must be sourced from the same directory" return 1 fi fi export PATH=$BASEDIR/scripts:$PATH export PYTHONPATH=$BASEDIR:$PYTHONPATH vcstool-0.3.0/stdeb.cfg000066400000000000000000000001661410413400700147620ustar00rootroot00000000000000[DEFAULT] No-Python2: Depends3: python3-setuptools, python3-yaml Conflicts3: python-vcstool X-Python3-Version: >= 3.5 vcstool-0.3.0/test/000077500000000000000000000000001410413400700141545ustar00rootroot00000000000000vcstool-0.3.0/test/branch.txt000066400000000000000000000002751410413400700161560ustar00rootroot00000000000000.... === ./immutable/hash (git) === (HEAD detached at 377d5b3) === ./immutable/tag (git) === (HEAD detached at 0.1.27) === ./vcstool (git) === master === ./without_version (git) === master vcstool-0.3.0/test/clients.txt000066400000000000000000000001021410413400700163470ustar00rootroot00000000000000The available VCS clients are: bzr git hg svn tar zip vcstool-0.3.0/test/commands.txt000066400000000000000000000001071410413400700165140ustar00rootroot00000000000000branch custom diff export import log pull push remotes status validate vcstool-0.3.0/test/custom_describe.txt000066400000000000000000000000721410413400700200660ustar00rootroot00000000000000.. === ./hash (git) === 0.1.26 === ./tag (git) === 0.1.27 vcstool-0.3.0/test/diff_hide.txt000066400000000000000000000005561410413400700166240ustar00rootroot00000000000000.... === ./immutable/hash (git) === diff --git a/LICENSE b/LICENSE index e5093df..c4f56b2 100644 --- a/LICENSE +++ b/LICENSE @@ -200,3 +200,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +testing \ No newline at end of file vcstool-0.3.0/test/export_exact.txt000066400000000000000000000004151410413400700174220ustar00rootroot00000000000000repositories: hash: type: git url: https://github.com/dirk-thomas/vcstool.git version: 377d5b3d03c212f015cc832fdb368f4534d0d583 tag: type: git url: https://github.com/dirk-thomas/vcstool.git version: bf9ca56de693a02b93ed423bcef589259d75eb0f vcstool-0.3.0/test/export_exact_with_tags.txt000066400000000000000000000003531410413400700214740ustar00rootroot00000000000000repositories: hash: type: git url: https://github.com/dirk-thomas/vcstool.git version: 377d5b3d03c212f015cc832fdb368f4534d0d583 tag: type: git url: https://github.com/dirk-thomas/vcstool.git version: 0.1.27 vcstool-0.3.0/test/import.txt000066400000000000000000000033301410413400700162260ustar00rootroot00000000000000...... === ./immutable/hash (git) === Cloning into '.'... Note: switching to '377d5b3d03c212f015cc832fdb368f4534d0d583'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at 377d5b3... update changelog === ./immutable/hash_tar (tar) === Downloaded tarball from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' and unpacked it === ./immutable/hash_zip (zip) === Downloaded zipfile from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' and unpacked it === ./immutable/tag (git) === Cloning into '.'... Note: switching to 'tags/0.1.27'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at bf9ca56... 0.1.27 === ./vcstool (git) === Cloning into '.'... === ./without_version (git) === Cloning into '.'... vcstool-0.3.0/test/import_shallow.txt000066400000000000000000000037651410413400700177730ustar00rootroot00000000000000...... === ./immutable/hash (git) === Initialized empty Git repository in ./immutable/hash/.git/ From https://github.com/dirk-thomas/vcstool * branch 377d5b3d03c212f015cc832fdb368f4534d0d583 -> FETCH_HEAD Note: switching to '377d5b3d03c212f015cc832fdb368f4534d0d583'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at 377d5b3... update changelog === ./immutable/hash_tar (tar) === Downloaded tarball from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' and unpacked it === ./immutable/hash_zip (zip) === Downloaded zipfile from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' and unpacked it === ./immutable/tag (git) === Initialized empty Git repository in ./immutable/tag/.git/ From https://github.com/dirk-thomas/vcstool * [new tag] 0.1.27 -> 0.1.27 Note: switching to 'tags/0.1.27'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at bf9ca56... 0.1.27 === ./vcstool (git) === Cloning into '.'... === ./without_version (git) === Cloning into '.'... vcstool-0.3.0/test/list.repos000066400000000000000000000015211410413400700162000ustar00rootroot00000000000000repositories: immutable/hash: type: git url: https://github.com/dirk-thomas/vcstool.git version: 377d5b3d03c212f015cc832fdb368f4534d0d583 immutable/hash_tar: type: tar url: https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz version: vcstool-377d5b3d03c212f015cc832fdb368f4534d0d583 immutable/hash_zip: type: zip url: https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip version: vcstool-377d5b3d03c212f015cc832fdb368f4534d0d583 immutable/tag: type: git url: https://github.com/dirk-thomas/vcstool.git version: tags/0.1.27 vcstool: type: git url: https://github.com/dirk-thomas/vcstool.git version: heads/master without_version: type: git url: https://github.com/dirk-thomas/vcstool.git vcstool-0.3.0/test/list2.repos000066400000000000000000000006201410413400700162610ustar00rootroot00000000000000repositories: hg/branch: type: hg url: https://www.mercurial-scm.org/repo/hg-stable version: stable hg/hash: type: hg url: https://www.mercurial-scm.org/repo/hg-stable version: 6d79894d3460 hg/tag: type: hg url: https://www.mercurial-scm.org/repo/hg-stable version: 5.8 svn/rev: type: svn url: https://github.com/dirk-thomas/vcstool version: 3 vcstool-0.3.0/test/log_limit.txt000066400000000000000000000011771410413400700167020ustar00rootroot00000000000000.. === ./hash (git) === commit 377d5b3d03c212f015cc832fdb368f4534d0d583 (HEAD) Author: Dirk Thomas update changelog commit f01ce3845fa0783bef9f97545e518e0f02cd509a Merge: 14f9968 e7770d3 Author: Dirk Thomas Merge pull request #44 from dirk-thomas/fix_loop_exit === ./tag (git) === commit bf9ca56de693a02b93ed423bcef589259d75eb0f (HEAD, tag: 0.1.27) Author: Dirk Thomas 0.1.27 commit 377d5b3d03c212f015cc832fdb368f4534d0d583 Author: Dirk Thomas update changelog vcstool-0.3.0/test/log_merges_only.txt000066400000000000000000000011351410413400700201010ustar00rootroot00000000000000=== . (git) === commit f01ce3845fa0783bef9f97545e518e0f02cd509a Merge: 14f9968 e7770d3 Author: Dirk Thomas Merge pull request #44 from dirk-thomas/fix_loop_exit commit 418cd63cc242aeb19bff17696adf1617b9c994b7 Merge: 6fdb8e8 89448bd Author: Dirk Thomas Merge pull request #42 from dirk-thomas/fix_depends_regression commit a3c4c33d9e958a5d297e2d1d777fe2d850b2566f Merge: a5dfaac 8a7101c Author: Dirk Thomas Merge pull request #41 from dirk-thomas/parent_path_dependencies vcstool-0.3.0/test/pull.txt000066400000000000000000000007121410413400700156710ustar00rootroot00000000000000.... === ./immutable/hash (git) === You are not currently on a branch. Please specify which branch you want to merge with. See git-pull(1) for details. git pull === ./immutable/tag (git) === You are not currently on a branch. Please specify which branch you want to merge with. See git-pull(1) for details. git pull === ./vcstool (git) === Already up to date. === ./without_version (git) === Already up to date. vcstool-0.3.0/test/reimport.txt000066400000000000000000000012611410413400700165560ustar00rootroot00000000000000...... === ./immutable/hash (git) === HEAD is now at 377d5b3... update changelog === ./immutable/hash_tar (tar) === Downloaded tarball from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' and unpacked it === ./immutable/hash_zip (zip) === Downloaded zipfile from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' and unpacked it === ./immutable/tag (git) === HEAD is now at bf9ca56... 0.1.27 === ./vcstool (git) === Already on 'master' Your branch is up to date with 'origin/master'. === ./without_version (git) === Switched to branch 'master' Your branch is up to date with 'origin/master'. vcstool-0.3.0/test/reimport_force.txt000066400000000000000000000011701410413400700177330ustar00rootroot00000000000000...... === ./immutable/hash (git) === HEAD is now at 377d5b3... update changelog === ./immutable/hash_tar (tar) === Downloaded tarball from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' and unpacked it === ./immutable/hash_zip (zip) === Downloaded zipfile from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' and unpacked it === ./immutable/tag (git) === HEAD is now at bf9ca56... 0.1.27 === ./vcstool (git) === Already on 'master' Your branch is up to date with 'origin/master'. === ./without_version (git) === Cloning into '.'... vcstool-0.3.0/test/reimport_skip.txt000066400000000000000000000007211410413400700176040ustar00rootroot00000000000000...... === ./immutable/hash (git) === === ./immutable/hash_tar (tar) === Downloaded tarball from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' and unpacked it === ./immutable/hash_zip (zip) === Downloaded zipfile from 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' and unpacked it === ./immutable/tag (git) === === ./vcstool (git) === === ./without_version (git) === vcstool-0.3.0/test/remotes_repos.txt000066400000000000000000000012331410413400700176020ustar00rootroot00000000000000./immutable/hash (git) ./immutable/tag (git) ./vcstool (git) ./without_version (git) .... === ./immutable/hash (git) === origin https://github.com/dirk-thomas/vcstool.git (fetch) origin https://github.com/dirk-thomas/vcstool.git (push) === ./immutable/tag (git) === origin https://github.com/dirk-thomas/vcstool.git (fetch) origin https://github.com/dirk-thomas/vcstool.git (push) === ./vcstool (git) === origin https://github.com/dirk-thomas/vcstool.git (fetch) origin https://github.com/dirk-thomas/vcstool.git (push) === ./without_version (git) === origin https://github.com/dirk-thomas/vcstool.git (fetch) origin https://github.com/dirk-thomas/vcstool.git (push) vcstool-0.3.0/test/status.txt000066400000000000000000000007071410413400700162440ustar00rootroot00000000000000.... === ./immutable/hash (git) === HEAD detached at 377d5b3 nothing to commit, working tree clean === ./immutable/tag (git) === HEAD detached at 0.1.27 nothing to commit, working tree clean === ./vcstool (git) === On branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean === ./without_version (git) === On branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean vcstool-0.3.0/test/test_commands.py000066400000000000000000000421571410413400700173770ustar00rootroot00000000000000import os from shutil import which import subprocess import sys import unittest sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from vcstool.clients.git import GitClient # noqa: E402 from vcstool.util import rmtree # noqa: E402 file_uri_scheme = 'file://' if sys.platform != 'win32' else 'file:///' REPOS_FILE = os.path.join(os.path.dirname(__file__), 'list.repos') REPOS_FILE_URL = file_uri_scheme + REPOS_FILE REPOS2_FILE = os.path.join(os.path.dirname(__file__), 'list2.repos') TEST_WORKSPACE = os.path.join( os.path.dirname(os.path.dirname(__file__)), 'test_workspace') CI = os.environ.get('CI') == 'true' # Travis CI / Github actions set: CI=true svn = which('svn') hg = which('hg') if svn: # check if the svn executable is usable (on macOS) # and not only exists to state that the program is not installed try: subprocess.check_call([svn, '--version']) except subprocess.CalledProcessError: svn = False class TestCommands(unittest.TestCase): @classmethod def setUpClass(cls): assert not os.path.exists(TEST_WORKSPACE) os.makedirs(TEST_WORKSPACE) try: output = run_command( 'import', ['--input', REPOS_FILE, '.']) expected = get_expected_output('import') # newer git versions don't append three dots after the commit hash assert output == expected or \ output == expected.replace(b'... ', b' ') except Exception: cls.tearDownClass() raise @classmethod def tearDownClass(cls): rmtree(TEST_WORKSPACE) def test_branch(self): output = run_command('branch') expected = get_expected_output('branch') self.assertEqual(output, expected) def test_custom(self): output = run_command( 'custom', args=['--git', '--args', 'describe', '--abbrev=0', '--tags'], subfolder='immutable') expected = get_expected_output('custom_describe') self.assertEqual(output, expected) def test_diff(self): license_path = os.path.join( TEST_WORKSPACE, 'immutable', 'hash', 'LICENSE') file_length = None try: with open(license_path, 'ab') as h: file_length = h.tell() h.write(b'testing') output = run_command('diff', args=['--hide']) expected = get_expected_output('diff_hide') finally: if file_length is not None: with open(license_path, 'ab') as h: h.truncate(file_length) self.assertEqual(output, expected) def test_export_exact_with_tags(self): output = run_command( 'export', args=['--exact-with-tags'], subfolder='immutable') expected = get_expected_output('export_exact_with_tags') self.assertEqual(output, expected) def test_export_exact(self): output = run_command( 'export', args=['--exact'], subfolder='immutable') expected = get_expected_output('export_exact') self.assertEqual(output, expected) def test_log(self): output = run_command( 'log', args=['--limit', '2'], subfolder='immutable') expected = get_expected_output('log_limit') self.assertEqual(output, expected) def test_log_merge_only(self): output = run_command( 'log', args=['--merge-only'], subfolder='immutable/tag') expected = get_expected_output('log_merges_only') self.assertEqual(output, expected) def test_pull(self): output = run_command('pull', args=['--workers', '1']) expected = get_expected_output('pull') # replace message from older git versions output = output.replace( b'anch. Please specify which\nbranch you want to merge with. See', b'anch.\nPlease specify which branch you want to merge with.\nSee') # newer git versions warn on pull with default config if GitClient.get_git_version() >= [2, 27, 0]: pull_warning = b""" warning: Pulling without specifying how to reconcile divergent branches is discouraged. You can squelch this message by running one of the following commands sometime before your next pull: git config pull.rebase false # merge (the default strategy) git config pull.rebase true # rebase git config pull.ff only # fast-forward only You can replace "git config" with "git config --global" to set a default preference for all repositories. You can also pass --rebase, --no-rebase, or --ff-only on the command line to override the configured default per invocation. """ output = output.replace(pull_warning, b'') self.assertEqual(output, expected) def test_pull_api(self): from io import StringIO from vcstool.commands.pull import main stdout_stderr = StringIO() # change and restore cwd cwd_bck = os.getcwd() os.chdir(TEST_WORKSPACE) try: # change and restore USE_COLOR flag from vcstool import executor use_color_bck = executor.USE_COLOR executor.USE_COLOR = False try: # change and restore os.environ env_bck = os.environ os.environ = dict(os.environ) os.environ.update( LANG='en_US.UTF-8', PYTHONPATH=( os.path.dirname(os.path.dirname(__file__)) + os.pathsep + os.environ.get('PYTHONPATH', ''))) try: rc = main( args=['--workers', '1'], stdout=stdout_stderr, stderr=stdout_stderr) finally: os.environ = env_bck finally: executor.USE_COLOR = use_color_bck finally: os.chdir(cwd_bck) assert rc == 0 # replace message from older git versions output = stdout_stderr.getvalue().replace( 'anch. Please specify which\nbranch you want to merge with. See', 'anch.\nPlease specify which branch you want to merge with.\nSee') # newer git versions warn on pull with default config if GitClient.get_git_version() >= [2, 27, 0]: pull_warning = """ warning: Pulling without specifying how to reconcile divergent branches is discouraged. You can squelch this message by running one of the following commands sometime before your next pull: git config pull.rebase false # merge (the default strategy) git config pull.rebase true # rebase git config pull.ff only # fast-forward only You can replace "git config" with "git config --global" to set a default preference for all repositories. You can also pass --rebase, --no-rebase, or --ff-only on the command line to override the configured default per invocation. """ output = output.replace(pull_warning, '') # the output was retrieved through a different way here output = adapt_command_output(output.encode()).decode() if sys.platform == 'win32': # it does not include carriage return characters on Windows output = output.replace('\n', '\r\n') expected = get_expected_output('pull').decode() assert output == expected def test_reimport(self): cwd_vcstool = os.path.join(TEST_WORKSPACE, 'vcstool') subprocess.check_output( ['git', 'remote', 'add', 'foo', 'http://foo.com/bar.git'], stderr=subprocess.STDOUT, cwd=cwd_vcstool) cwd_without_version = os.path.join(TEST_WORKSPACE, 'without_version') subprocess.check_output( ['git', 'checkout', '-b', 'foo'], stderr=subprocess.STDOUT, cwd=cwd_without_version) output = run_command( 'import', ['--skip-existing', '--input', REPOS_FILE, '.']) expected = get_expected_output('reimport_skip') # newer git versions don't append three dots after the commit hash assert output == expected or output == expected.replace(b'... ', b' ') subprocess.check_output( ['git', 'remote', 'set-url', 'origin', 'http://foo.com/bar.git'], stderr=subprocess.STDOUT, cwd=cwd_without_version) run_command( 'import', ['--skip-existing', '--input', REPOS_FILE, '.']) output = run_command( 'import', ['--force', '--input', REPOS_FILE, '.']) expected = get_expected_output('reimport_force') # on Windows, the "Already on 'master'" message is after the # "Your branch is up to date with ..." message, so remove it # from both output and expected strings if sys.platform == 'win32': output = output.replace(b"Already on 'master'\r\n", b'') expected = expected.replace(b"Already on 'master'\r\n", b'') # newer git versions don't append three dots after the commit hash assert output == expected or output == expected.replace(b'... ', b' ') subprocess.check_output( ['git', 'remote', 'remove', 'foo'], stderr=subprocess.STDOUT, cwd=cwd_vcstool) def test_reimport_failed(self): cwd_tag = os.path.join(TEST_WORKSPACE, 'immutable', 'tag') subprocess.check_output( ['git', 'remote', 'add', 'foo', 'http://foo.com/bar.git'], stderr=subprocess.STDOUT, cwd=cwd_tag) subprocess.check_output( ['git', 'remote', 'rm', 'origin'], stderr=subprocess.STDOUT, cwd=cwd_tag) try: run_command( 'import', ['--skip-existing', '--input', REPOS_FILE, '.']) finally: subprocess.check_output( ['git', 'remote', 'rm', 'foo'], stderr=subprocess.STDOUT, cwd=cwd_tag) subprocess.check_output( ['git', 'remote', 'add', 'origin', 'https://github.com/dirk-thomas/vcstool.git'], stderr=subprocess.STDOUT, cwd=cwd_tag) def test_import_force_non_empty(self): workdir = os.path.join(TEST_WORKSPACE, 'force-non-empty') os.makedirs(os.path.join(workdir, 'vcstool', 'not-a-git-repo')) try: output = run_command( 'import', ['--force', '--input', REPOS_FILE, '.'], subfolder='force-non-empty') expected = get_expected_output('import') # newer git versions don't append ... after the commit hash assert ( output == expected or output == expected.replace(b'... ', b' ')) finally: rmtree(workdir) def test_import_shallow(self): workdir = os.path.join(TEST_WORKSPACE, 'import-shallow') os.makedirs(workdir) try: output = run_command( 'import', ['--shallow', '--input', REPOS_FILE, '.'], subfolder='import-shallow') # the actual output contains absolute paths output = output.replace( b'repository in ' + workdir.encode() + b'/', b'repository in ./') expected = get_expected_output('import_shallow') # newer git versions don't append ... after the commit hash assert ( output == expected or output == expected.replace(b'... ', b' ')) # check that repository history has only one commit output = subprocess.check_output( ['git', 'log', '--format=oneline'], stderr=subprocess.STDOUT, cwd=os.path.join(workdir, 'vcstool')) assert len(output.splitlines()) == 1 finally: rmtree(workdir) def test_import_url(self): workdir = os.path.join(TEST_WORKSPACE, 'import-url') os.makedirs(workdir) try: output = run_command( 'import', ['--input', REPOS_FILE_URL, '.'], subfolder='import-url') # the actual output contains absolute paths output = output.replace( b'repository in ' + workdir.encode() + b'/', b'repository in ./') expected = get_expected_output('import') # newer git versions don't append ... after the commit hash assert ( output == expected or output == expected.replace(b'... ', b' ')) finally: rmtree(workdir) def test_validate(self): output = run_command( 'validate', ['--input', REPOS_FILE]) expected = get_expected_output('validate') self.assertEqual(output, expected) output = run_command( 'validate', ['--hide-empty', '--input', REPOS_FILE]) expected = get_expected_output('validate_hide') self.assertEqual(output, expected) @unittest.skipIf(not svn and not CI, '`svn` was not found') @unittest.skipIf(not hg and not CI, '`hg` was not found') def test_validate_svn_and_hg(self): output = run_command( 'validate', ['--input', REPOS2_FILE]) expected = get_expected_output('validate2') self.assertEqual(output, expected) def test_remote(self): output = run_command('remotes', args=['--repos']) expected = get_expected_output('remotes_repos') self.assertEqual(output, expected) def test_status(self): output = run_command('status') # replace message from older git versions # https://github.com/git/git/blob/3ec7d702a89c647ddf42a59bc3539361367de9d5/Documentation/RelNotes/2.10.0.txt#L373-L374 output = output.replace( b'working directory clean', b'working tree clean') # the following seems to have changed between git 2.10.0 and 2.14.1 output = output.replace( b'.\nnothing to commit', b'.\n\nnothing to commit') expected = get_expected_output('status') self.assertEqual(output, expected) def run_command(command, args=None, subfolder=None): repo_root = os.path.dirname(os.path.dirname(__file__)) script = os.path.join(repo_root, 'scripts', 'vcs-' + command) env = dict(os.environ) env.update( LANG='en_US.UTF-8', PYTHONPATH=repo_root + os.pathsep + env.get('PYTHONPATH', '')) cwd = TEST_WORKSPACE if subfolder: cwd = os.path.join(cwd, subfolder) output = subprocess.check_output( [sys.executable, script] + (args or []), stderr=subprocess.STDOUT, cwd=cwd, env=env) return adapt_command_output(output, cwd) def adapt_command_output(output, cwd=None): assert type(output) == bytes # replace message from older git versions output = output.replace( b'git checkout -b new_branch_name', b'git checkout -b ') output = output.replace( b'(detached from ', b'(HEAD detached at ') output = output.replace( b"ady on 'master'\n=", b"ady on 'master'\nYour branch is up-to-date with 'origin/master'.\n=") output = output.replace( b'# HEAD detached at ', b'HEAD detached at ') output = output.replace( b'# On branch master', b"On branch master\nYour branch is up-to-date with 'origin/master'.\n") # the following seems to have changed between git 2.17.1 and 2.25.1 output = output.replace( b"Note: checking out '", b"Note: switching to '") output = output.replace( b'by performing another checkout.', b'by switching back to a branch.') output = output.replace( b'using -b with the checkout command again.', b'using -c with the switch command.') output = output.replace( b'git checkout -b ', b'git switch -c \n\n' b'Or undo this operation with:\n\n' b' git switch -\n\n' b'Turn off this advice by setting config variable ' b'advice.detachedHead to false') # replace GitHub SSH clone URL output = output.replace( b'git@github.com:', b'https://github.com/') if sys.platform == 'win32': if cwd: # on Windows, git prints full path to repos # in some messages, so make it relative cwd_abs = os.path.abspath(cwd).replace('\\', '/') output = output.replace(cwd_abs.encode(), b'.') # replace path separators in specific paths; # this is less likely to cause wrong test results paths_to_replace = [ (b'.\\immutable', b'./immutable'), (b'.\\vcstool', b'./vcstool'), (b'.\\without_version', b'./without_version'), (b'\\hash', b'/hash'), (b'\\tag', b'/tag'), ] for before, after in paths_to_replace: output = output.replace(before, after) return output def get_expected_output(name): path = os.path.join(os.path.dirname(__file__), name + '.txt') with open(path, 'rb') as h: content = h.read() # change in git version 2.15.0 # https://github.com/git/git/commit/7560f547e6 if GitClient.get_git_version() < [2, 15, 0]: # use hyphenation for older git versions content = content.replace(b'up to date', b'up-to-date') return content if __name__ == '__main__': unittest.main() vcstool-0.3.0/test/test_flake8.py000066400000000000000000000045121410413400700167410ustar00rootroot00000000000000import logging import os from flake8 import configure_logging from flake8.api.legacy import StyleGuide from flake8.main.application import Application from pydocstyle.config import log log.level = logging.INFO def test_flake8(): configure_logging(1) argv = [ '--extend-ignore=' + ','.join([ 'A003', 'D100', 'D101', 'D102', 'D103', 'D104', 'D105', 'D107']), '--exclude', 'vcstool/compat/shutil.py', '--import-order-style=google'] style_guide = get_style_guide(argv) base_path = os.path.join(os.path.dirname(__file__), '..') paths = [ os.path.join(base_path, 'setup.py'), os.path.join(base_path, 'test'), os.path.join(base_path, 'vcstool'), ] scripts_path = os.path.join(base_path, 'scripts') for script in os.listdir(scripts_path): if script.startswith('.'): continue paths.append(os.path.join(scripts_path, script)) report = style_guide.check_files(paths) assert report.total_errors == 0, \ 'Found %d code style warnings' % report.total_errors def get_style_guide(argv=None): # this is a fork of flake8.api.legacy.get_style_guide # to allow passing command line argument application = Application() if hasattr(application, 'parse_preliminary_options'): prelim_opts, remaining_args = application.parse_preliminary_options( argv) from flake8 import configure_logging configure_logging(prelim_opts.verbose, prelim_opts.output_file) from flake8.options import config config_finder = config.ConfigFileFinder( application.program, prelim_opts.append_config, config_file=prelim_opts.config, ignore_config_files=prelim_opts.isolated) application.find_plugins(config_finder) application.register_plugin_options() application.parse_configuration_and_cli(config_finder, remaining_args) else: application.parse_preliminary_options_and_args([]) application.make_config_finder() application.find_plugins() application.register_plugin_options() application.parse_configuration_and_cli(argv) application.make_formatter() application.make_guide() application.make_file_checker_manager() return StyleGuide(application) if __name__ == '__main__': test_flake8() vcstool-0.3.0/test/test_options.py000066400000000000000000000020231410413400700172550ustar00rootroot00000000000000import os import subprocess import sys import unittest class TestOptions(unittest.TestCase): def test_clients(self): output = run_command(['--clients']) expected = get_expected_output('clients') self.assertEqual(output, expected) def test_commands(self): output = run_command(['--commands']) expected = get_expected_output('commands') self.assertEqual(output, expected) def run_command(args): repo_root = os.path.dirname(os.path.dirname(__file__)) script = os.path.join(repo_root, 'scripts', 'vcs') env = dict(os.environ) env.update( LANG='en_US.UTF-8', PYTHONPATH=repo_root + os.pathsep + env.get('PYTHONPATH', '')) return subprocess.check_output( [sys.executable, script] + (args or []), stderr=subprocess.STDOUT, env=env) def get_expected_output(name): path = os.path.join(os.path.dirname(__file__), name + '.txt') with open(path, 'rb') as h: return h.read() if __name__ == '__main__': unittest.main() vcstool-0.3.0/test/validate.txt000066400000000000000000000015141410413400700165070ustar00rootroot00000000000000...... === immutable/hash (git) === Found git repository 'https://github.com/dirk-thomas/vcstool.git' but unable to verify non-branch / non-tag ref '377d5b3d03c212f015cc832fdb368f4534d0d583' without cloning the repo === immutable/hash_tar (tar) === Tarball url 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.tar.gz' exists === immutable/hash_zip (zip) === Zip url 'https://github.com/dirk-thomas/vcstool/archive/377d5b3d03c212f015cc832fdb368f4534d0d583.zip' exists === immutable/tag (git) === Found git repository 'https://github.com/dirk-thomas/vcstool.git' with tag '0.1.27' === vcstool (git) === Found git repository 'https://github.com/dirk-thomas/vcstool.git' with branch 'master' === without_version (git) === Found git repository 'https://github.com/dirk-thomas/vcstool.git' with default branch vcstool-0.3.0/test/validate2.txt000066400000000000000000000006771410413400700166020ustar00rootroot00000000000000.... === hg/branch (hg) === Found hg repository 'https://www.mercurial-scm.org/repo/hg-stable' with changeset 'stable' === hg/hash (hg) === Found hg repository 'https://www.mercurial-scm.org/repo/hg-stable' with changeset '6d79894d3460' === hg/tag (hg) === Found hg repository 'https://www.mercurial-scm.org/repo/hg-stable' with changeset '5.8' === svn/rev (svn) === Found svn repository 'https://github.com/dirk-thomas/vcstool' with revision '3' vcstool-0.3.0/test/validate_hide.txt000066400000000000000000000003301410413400700174730ustar00rootroot00000000000000...... === immutable/hash (git) === Found git repository 'https://github.com/dirk-thomas/vcstool.git' but unable to verify non-branch / non-tag ref '377d5b3d03c212f015cc832fdb368f4534d0d583' without cloning the repo vcstool-0.3.0/vcstool-completion/000077500000000000000000000000001410413400700170355ustar00rootroot00000000000000vcstool-0.3.0/vcstool-completion/vcs.bash000066400000000000000000000003421410413400700204660ustar00rootroot00000000000000function _vcs() { local cur COMPREPLY=() cur=${COMP_WORDS[COMP_CWORD]} if [ $COMP_CWORD -eq 1 ]; then COMPREPLY=( $( compgen -W "`vcs --commands`" -- $cur )) fi } complete -o dirnames -F _vcs vcs vcstool-0.3.0/vcstool-completion/vcs.fish000066400000000000000000000007371410413400700205120ustar00rootroot00000000000000complete -xc vcs -n '__fish_use_subcommand' -a '(vcs --commands-descriptions)' complete -xc vcs -s h -l help -d 'show this help message and exit' complete -xc vcs -l clients -d 'Show the available VCS clients' complete -xc vcs -l commands -d 'Output the available commands for auto-completion' complete -xc vcs -l commands-descriptions -d 'Output the available commands along with their descriptions for auto-completion' complete -xc vcs -l version -d 'Show the vcstool version' vcstool-0.3.0/vcstool-completion/vcs.tcsh000066400000000000000000000000561410413400700205140ustar00rootroot00000000000000complete vcs 'p/1/`vcs --commands`/' 'C/*/d/' vcstool-0.3.0/vcstool-completion/vcs.zsh000066400000000000000000000002531410413400700203560ustar00rootroot00000000000000function _vcs() { local opts reply=() if [[ ${CURRENT} == 2 ]]; then opts=`vcs --commands` reply=(${=opts}) fi } compctl -K "_vcs" "vcs" vcstool-0.3.0/vcstool/000077500000000000000000000000001410413400700146665ustar00rootroot00000000000000vcstool-0.3.0/vcstool/__init__.py000066400000000000000000000001041410413400700167720ustar00rootroot00000000000000from .clients import vcstool_clients # noqa __version__ = '0.3.0' vcstool-0.3.0/vcstool/clients/000077500000000000000000000000001410413400700163275ustar00rootroot00000000000000vcstool-0.3.0/vcstool/clients/__init__.py000066400000000000000000000015401410413400700204400ustar00rootroot00000000000000vcstool_clients = [] try: from .bzr import BzrClient vcstool_clients.append(BzrClient) except ImportError: pass try: from .git import GitClient vcstool_clients.append(GitClient) except ImportError: pass try: from .hg import HgClient vcstool_clients.append(HgClient) except ImportError: pass try: from .svn import SvnClient vcstool_clients.append(SvnClient) except ImportError: pass try: from .tar import TarClient vcstool_clients.append(TarClient) except ImportError: pass try: from .zip import ZipClient vcstool_clients.append(ZipClient) except ImportError: pass _client_types = [c.type for c in vcstool_clients] if len(_client_types) != len(set(_client_types)): raise RuntimeError( 'Multiple vcs clients share the same type: ' + ', '.join(sorted(_client_types))) vcstool-0.3.0/vcstool/clients/bzr.py000066400000000000000000000160011410413400700174740ustar00rootroot00000000000000import copy import os from shutil import which from .vcs_base import VcsClientBase from ..util import rmtree class BzrClient(VcsClientBase): type = 'bzr' _executable = None @staticmethod def is_repository(path): return os.path.isdir(os.path.join(path, '.bzr')) def __init__(self, path): super(BzrClient, self).__init__(path) def branch(self, command): if command.all: return self._not_applicable( command, message='at least with the option to list all branches') self._check_executable() return self._get_parent_branch() def custom(self, command): self._check_executable() cmd = [BzrClient._executable] + command.args return self._run_command(cmd) def diff(self, _command): self._check_executable() cmd = [BzrClient._executable, 'diff'] return self._run_command(cmd) def import_(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } self._check_executable() if BzrClient.is_repository(self.path): # verify that existing repository is the same result_parent_branch = self._get_parent_branch() if result_parent_branch['returncode']: return result_parent_branch parent_branch = result_parent_branch['output'] if parent_branch != command.url: if not command.force: return { 'cmd': '', 'cwd': self.path, 'output': 'Path already exists and contains a different ' 'repository', 'returncode': 1 } try: rmtree(self.path) except OSError: os.remove(self.path) not_exist = self._create_path() if not_exist: return not_exist if BzrClient.is_repository(self.path): # pull updates for existing repo cmd_pull = [BzrClient._executable, 'pull'] return self._run_command(cmd_pull, retry=command.retry) else: cmd_branch = [BzrClient._executable, 'branch'] if command.version: cmd_branch += ['-r', command.version] cmd_branch += [command.url, '.'] result_branch = self._run_command(cmd_branch, retry=command.retry) if result_branch['returncode']: result_branch['output'] = \ "Could not branch repository '%s': %s" % \ (command.url, result_branch['output']) return result_branch return result_branch def log(self, command): self._check_executable() if command.limit_tag or command.limit_untagged: tag = None if command.limit_tag: tag = command.limit_tag else: # determine nearest tag cmd_tag = [BzrClient._executable, 'tags', '--sort=time'] result_tag = self._run_command(cmd_tag) if result_tag['returncode']: return result_tag for line in result_tag['output'].splitlines(): parts = line.split(' ', 2) if parts[1] != '?': tag = parts[0] if not tag: result_tag['output'] = 'Could not determine latest tag', result_tag['returncode'] = 1 return result_tag # determine revision number of tag cmd_tag_rev = [ BzrClient._executable, 'revno', '--rev', 'tag:' + tag] result_tag_rev = self._run_command(cmd_tag_rev) if result_tag_rev['returncode']: if command.limit_tag: result_tag_rev['output'] = \ "Repository lacks the tag '%s'" % tag return result_tag_rev try: tag_rev = int(result_tag_rev['output']) tag_next_rev = tag_rev + 1 except ValueError: tag_rev = result_tag_rev['output'] tag_next_rev = tag_rev # determine revision number of HEAD cmd_head_rev = [BzrClient._executable, 'revno'] result_head_rev = self._run_command(cmd_head_rev) if result_head_rev['returncode']: return result_head_rev try: head_rev = int(result_head_rev['output']) except ValueError: head_rev = result_head_rev['output'] # output log since nearest tag cmd_log = [ BzrClient._executable, 'log', '--rev', 'revno:%s..' % str(tag_next_rev)] if tag_rev == head_rev: return { 'cmd': ' '.join(cmd_log), 'cwd': self.path, 'output': '', 'returncode': 0 } if command.limit != 0: cmd_log += ['--limit', '%d' % command.limit] result_log = self._run_command(cmd_log) return result_log cmd = [BzrClient._executable, 'log'] if command.limit != 0: cmd += ['--limit', '%d' % command.limit] return self._run_command(cmd) def pull(self, _command): self._check_executable() cmd = [BzrClient._executable, 'pull'] return self._run_command(cmd) def push(self, _command): self._check_executable() cmd = [BzrClient._executable, 'push'] return self._run_command(cmd) def remotes(self, _command): self._check_executable() return self._get_parent_branch() def status(self, _command): self._check_executable() cmd = [BzrClient._executable, 'status'] return self._run_command(cmd) def _get_parent_branch(self): cmd = [BzrClient._executable, 'info'] # parsing the text output requires enforcing language env = copy.copy(os.environ) env['LANG'] = 'en_US.UTF-8' result = self._run_command(cmd, env) if result['returncode']: return result branch = None prefix = ' parent branch: ' for line in result['output'].splitlines(): if line.startswith(prefix): branch = line[len(prefix):] break if not branch: result['output'] = 'Could not determine parent branch', result['returncode'] = 1 return result result['output'] = branch return result def _check_executable(self): assert BzrClient._executable is not None, \ "Could not find 'bzr' executable" if not BzrClient._executable: BzrClient._executable = which('bzr') vcstool-0.3.0/vcstool/clients/git.py000066400000000000000000000753431410413400700175000ustar00rootroot00000000000000import os from shutil import which import subprocess from vcstool.executor import USE_COLOR from .vcs_base import VcsClientBase from ..util import rmtree class GitClient(VcsClientBase): type = 'git' _executable = None _git_version = None _config_color_is_auto = None @classmethod def get_git_version(cls): if cls._git_version is None: output = subprocess.check_output(['git', '--version']) prefix = b'git version ' assert output.startswith(prefix) output = output[len(prefix):].split(maxsplit=1)[0] cls._git_version = [ int(x) for x in output.split(b'.') if x != b'windows'] return cls._git_version @staticmethod def is_repository(path): return os.path.isdir(os.path.join(path, '.git')) def __init__(self, path): super(GitClient, self).__init__(path) def branch(self, command): self._check_executable() cmd = [GitClient._executable, 'branch'] result = self._run_command(cmd) if not command.all and not result['returncode']: # only show current branch lines = result['output'].splitlines() lines = [line[2:] for line in lines if line.startswith('* ')] result['output'] = '\n'.join(lines) return result def custom(self, command): self._check_executable() cmd = [GitClient._executable] + command.args return self._run_command(cmd) def diff(self, command): self._check_executable() cmd = [GitClient._executable, 'diff'] self._check_color(cmd) if command.context: cmd += ['--unified=%d' % command.context] return self._run_command(cmd) def export(self, command): self._check_executable() exact = command.exact if not exact: # determine if a specific branch is checked out or ec is detached cmd_branch = [ GitClient._executable, 'rev-parse', '--abbrev-ref', 'HEAD'] result_branch = self._run_command(cmd_branch) if result_branch['returncode']: result_branch['output'] = 'Could not determine ref: ' + \ result_branch['output'] return result_branch branch_name = result_branch['output'] exact = branch_name == 'HEAD' # is detached if not exact: # determine the remote of the current branch cmd_remote = [ GitClient._executable, 'rev-parse', '--abbrev-ref', '@{upstream}'] result_remote = self._run_command(cmd_remote) if result_remote['returncode']: result_remote['output'] = 'Could not determine ref: ' + \ result_remote['output'] return result_remote branch_with_remote = result_remote['output'] # determine remote suffix = '/' + branch_name assert branch_with_remote.endswith(branch_name), \ "'%s' does not end with '%s'" % \ (branch_with_remote, branch_name) remote = branch_with_remote[:-len(suffix)] # if a local ref exists with the same name as the remote branch # the result will be prefixed to make it unambiguous prefix = 'remotes/' if remote.startswith(prefix): remote = remote[len(prefix):] # determine url of remote result_url = self._get_remote_url(remote) if result_url['returncode']: return result_url url = result_url['output'] # the result is the remote url and the branch name return { 'cmd': ' && '.join([ result_branch['cmd'], result_remote['cmd'], result_url['cmd']]), 'cwd': self.path, 'output': '\n'.join([url, branch_name]), 'returncode': 0, 'export_data': {'url': url, 'version': branch_name} } else: # determine the hash cmd_ref = [GitClient._executable, 'rev-parse', 'HEAD'] result_ref = self._run_command(cmd_ref) if result_ref['returncode']: result_ref['output'] = 'Could not determine ref: ' + \ result_ref['output'] return result_ref ref = result_ref['output'] # get all remote names cmd_remotes = [GitClient._executable, 'remote'] result_remotes = self._run_command(cmd_remotes) if result_remotes['returncode']: result_remotes['output'] = 'Could not determine remotes: ' + \ result_remotes['output'] return result_remotes remotes = result_remotes['output'].splitlines() # prefer origin and upstream remotes if 'upstream' in remotes: remotes.remove('upstream') remotes.insert(0, 'upstream') if 'origin' in remotes: remotes.remove('origin') remotes.insert(0, 'origin') # for each remote name check if the hash is part of the remote for remote in remotes: # get all remote names cmd_refs = [ GitClient._executable, 'rev-list', '--remotes=' + remote, '--tags'] result_refs = self._run_command(cmd_refs) if result_refs['returncode']: result_refs['output'] = \ "Could not determine refs of remote '%s': " % \ remote + result_refs['output'] return result_refs refs = result_refs['output'].splitlines() if ref not in refs: continue cmds = [result_ref['cmd']] if command.with_tags: # check if there is exactly one tag pointing to that ref cmd_tags = [ GitClient._executable, 'tag', '--points-at', ref] result_tags = self._run_command(cmd_tags) if result_tags['returncode']: result_tags['output'] = \ "Could not determine tags for ref '%s': " % \ ref + result_tags['output'] return result_tags cmds.append(result_tags['cmd']) tags = result_tags['output'].splitlines() if len(tags) == 1: tag = tags[0] # double check that the tag is part of the remote # and references the same hash cmd_ls_remote = [ GitClient._executable, 'ls-remote', remote, 'refs/tags/' + tag] result_ls_remote = self._run_command(cmd_ls_remote) if result_ls_remote['returncode']: result_ls_remote['output'] = \ "Could not check remote tags for '%s': " % \ remote + result_ls_remote['output'] return result_ls_remote matches = self._get_hash_ref_tuples( result_ls_remote['output']) if len(matches) == 1 and matches[0][0] == ref: ref = tag # determine url of remote result_url = self._get_remote_url(remote) if result_url['returncode']: return result_url url = result_url['output'] cmds.append(result_url['cmd']) # the result is the remote url and the hash/tag return { 'cmd': ' && '.join(cmds), 'cwd': self.path, 'output': '\n'.join([url, ref]), 'returncode': 0, 'export_data': {'url': url, 'version': ref} } return { 'cmd': ' && '.join([result_ref['cmd'], result_remotes['cmd']]), 'cwd': self.path, 'output': "Could not determine remote containing '%s'" % ref, 'returncode': 1, } def _get_remote_url(self, remote): cmd_url = [ GitClient._executable, 'config', '--get', 'remote.%s.url' % remote] result_url = self._run_command(cmd_url) if result_url['returncode']: result_url['output'] = 'Could not determine remote url: ' + \ result_url['output'] return result_url def import_(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } self._check_executable() if GitClient.is_repository(self.path): # verify that existing repository is the same result_urls = self._get_remote_urls() if result_urls['returncode']: return result_urls for url, remote in result_urls['output']: if url == command.url: break else: if command.skip_existing: return { 'cmd': '', 'cwd': self.path, 'output': 'Skipped existing repository with different URL', 'returncode': 0 } if not command.force: return { 'cmd': '', 'cwd': self.path, 'output': 'Path already exists and contains a different ' 'repository', 'returncode': 1 } try: rmtree(self.path) except OSError: os.remove(self.path) elif command.skip_existing and os.path.exists(self.path): return { 'cmd': '', 'cwd': self.path, 'output': 'Skipped existing directory', 'returncode': 0 } elif command.force and os.path.exists(self.path): # Not empty, not a git repository try: rmtree(self.path) except OSError: os.remove(self.path) not_exist = self._create_path() if not_exist: return not_exist if GitClient.is_repository(self.path): if command.skip_existing: checkout_version = None elif command.version: checkout_version = command.version else: # determine remote HEAD branch cmd_remote = [GitClient._executable, 'remote', 'show', remote] # override locale in order to parse output env = os.environ.copy() env['LC_ALL'] = 'C' result_remote = self._run_command(cmd_remote, env=env) if result_remote['returncode']: result_remote['output'] = \ 'Could not get remote information of repository ' \ "'%s': %s" % (url, result_remote['output']) return result_remote prefix = ' HEAD branch: ' for line in result_remote['output'].splitlines(): if line.startswith(prefix): checkout_version = line[len(prefix):] break else: result_remote['returncode'] = 1 result_remote['output'] = \ 'Could not determine remote HEAD branch of ' \ "repository '%s': %s" % (url, result_remote['output']) return result_remote # fetch updates for existing repo cmd_fetch = [GitClient._executable, 'fetch', remote] if command.shallow: result_version_type, version_name = self._check_version_type( command.url, checkout_version) if result_version_type['returncode']: return result_version_type version_type = result_version_type['version_type'] if version_type == 'branch': cmd_fetch.append( 'refs/heads/%s:refs/remotes/%s/%s' % (version_name, remote, version_name)) elif version_type == 'hash': cmd_fetch.append(checkout_version) elif version_type == 'tag': cmd_fetch.append( '+refs/tags/%s:refs/tags/%s' % (version_name, version_name)) else: assert False cmd_fetch += ['--depth', '1'] else: version_type = None result_fetch = self._run_command(cmd_fetch, retry=command.retry) if result_fetch['returncode']: return result_fetch cmd = result_fetch['cmd'] output = result_fetch['output'] if not command.shallow and checkout_version is not None: if checkout_version.startswith('heads/'): version_name = checkout_version[6:] version_type = 'branch' elif checkout_version.startswith('tags/'): version_name = checkout_version[5:] version_type = 'tag' else: version_type = None # ensure that a tracking branch exists which can be checked out if version_type == 'branch': cmd_show_ref = [ GitClient._executable, 'show-ref', 'refs/heads/%s' % version_name] result_show_ref = self._run_command(cmd_show_ref) if result_show_ref['returncode']: if not command.shallow: result_show_ref['output'] = \ "Could not find branch '%s': %s" % \ (version_name, result_show_ref['output']) return result_show_ref # creating tracking branch cmd_branch = [ GitClient._executable, 'branch', version_name, '%s/%s' % (remote, version_name)] result_branch = self._run_command(cmd_branch) if result_branch['returncode']: result_branch['output'] = \ "Could not create branch '%s': %s" % \ (version_name, result_branch['output']) return result_branch cmd += ' && ' + ' '.join(cmd_branch) output = '\n'.join([output, result_branch['output']]) checkout_version = version_name else: version_type = None if command.version: result_version_type, version_name = self._check_version_type( command.url, command.version) if result_version_type['returncode']: return result_version_type version_type = result_version_type['version_type'] if not command.shallow or version_type in (None, 'branch'): cmd_clone = [GitClient._executable, 'clone', command.url, '.'] if version_type == 'branch': cmd_clone += ['-b', version_name] checkout_version = None else: checkout_version = command.version if command.shallow: cmd_clone += ['--depth', '1'] result_clone = self._run_command( cmd_clone, retry=command.retry) if result_clone['returncode']: result_clone['output'] = \ "Could not clone repository '%s': %s" % \ (command.url, result_clone['output']) return result_clone cmd = result_clone['cmd'] output = result_clone['output'] else: # getting a hash or tag with a depth of 1 can't use 'clone' cmd_init = [GitClient._executable, 'init'] result_init = self._run_command(cmd_init) if result_init['returncode']: return result_init cmd = result_init['cmd'] output = result_init['output'] cmd_remote_add = [ GitClient._executable, 'remote', 'add', 'origin', command.url] result_remote_add = self._run_command(cmd_remote_add) if result_remote_add['returncode']: return result_remote_add cmd += ' && ' + ' '.join(cmd_remote_add) output = '\n'.join([output, result_remote_add['output']]) cmd_fetch = [GitClient._executable, 'fetch', 'origin'] if version_type == 'hash': cmd_fetch.append(command.version) elif version_type == 'tag': cmd_fetch.append( 'refs/tags/%s:refs/tags/%s' % (version_name, version_name)) else: assert False cmd_fetch += ['--depth', '1'] result_fetch = self._run_command( cmd_fetch, retry=command.retry) if result_fetch['returncode']: return result_fetch cmd += ' && ' + ' '.join(cmd_fetch) output = '\n'.join([output, result_fetch['output']]) checkout_version = command.version if checkout_version: cmd_checkout = [ GitClient._executable, 'checkout', checkout_version, '--'] result_checkout = self._run_command(cmd_checkout) if result_checkout['returncode']: if self.get_git_version() < [1, 8, 4, 3]: cmd_checkout.pop() result_checkout = self._run_command(cmd_checkout) if result_checkout['returncode']: result_checkout['output'] = \ "Could not checkout ref '%s': %s" % \ (checkout_version, result_checkout['output']) return result_checkout cmd += ' && ' + ' '.join(cmd_checkout) output = '\n'.join([output, result_checkout['output']]) if command.recursive: cmd_submodule = [ GitClient._executable, 'submodule', 'update', '--init', '--recursive'] result_submodule = self._run_command(cmd_submodule) if result_submodule['returncode']: result_submodule['output'] = \ 'Could not init/update submodules: %s' % \ result_submodule['output'] return result_submodule cmd += ' && ' + ' '.join(cmd_submodule) output = '\n'.join([output, result_submodule['output']]) return { 'cmd': cmd, 'cwd': self.path, 'output': output, 'returncode': 0 } def _get_remote_urls(self): cmd_remote = [GitClient._executable, 'remote', 'show'] result_remote = self._run_command(cmd_remote) if result_remote['returncode']: result_remote['output'] = 'Could not determine remotes: ' + \ result_remote['output'] return result_remote remote_urls = [] cmd = result_remote['cmd'] for remote in result_remote['output'].splitlines(): result_url = self._get_remote_url(remote) cmd += ' && ' + result_url['cmd'] if not result_url['returncode']: remote_urls.append((result_url['output'], remote)) return { 'cmd': cmd, 'cwd': self.path, 'output': (remote_urls if remote_urls else 'Could not determine any of the remote urls'), 'returncode': 0 if remote_urls else 1 } def _check_version_type(self, url, version): # check if version starts with heads/ or tags/ prefixes = { 'heads/': 'branch', 'tags/': 'tag', } for prefix, version_type in prefixes.items(): if version.startswith(prefix): return { 'cmd': None, 'cwd': None, 'output': None, 'returncode': 0, 'version_type': version_type, }, version[len(prefix):] cmd = [GitClient._executable, 'ls-remote', url, version] result = self._run_command(cmd) if result['returncode']: result['output'] = 'Could not determine ref type of version: ' + \ result['output'] return result, None if not result['output']: result['version_type'] = 'hash' return result, None refs = {} for hash_, ref in self._get_hash_ref_tuples(result['output']): refs[ref] = hash_ tag_ref = 'refs/tags/' + version branch_ref = 'refs/heads/' + version if tag_ref in refs and branch_ref in refs: if refs[tag_ref] != refs[branch_ref]: result['returncode'] = 1 result['output'] = 'The version ref is a branch as well as ' \ 'tag but with different hashes' return result, None if tag_ref in refs: result['version_type'] = 'tag' elif branch_ref in refs: result['version_type'] = 'branch' else: result['version_type'] = 'hash' return result, \ version if result['version_type'] in ('tag', 'branch') else None def log(self, command): self._check_executable() if command.limit_tag: # check if specific tag exists cmd_tag = [GitClient._executable, 'tag', '-l', command.limit_tag] result_tag = self._run_command(cmd_tag) if result_tag['returncode']: return result_tag if not result_tag['output']: return { 'cmd': '', 'cwd': self.path, 'output': "Repository lacks the tag '%s'" % command.limit_tag, 'returncode': 1 } # output log since specific tag cmd = [GitClient._executable, 'log', '%s..' % command.limit_tag] elif command.limit_untagged: # determine nearest tag cmd_tag = [ GitClient._executable, 'describe', '--abbrev=0', '--tags'] result_tag = self._run_command(cmd_tag) if result_tag['returncode']: return result_tag # output log since nearest tag cmd = [GitClient._executable, 'log', '%s..' % result_tag['output']] else: cmd = [GitClient._executable, 'log'] if command.merge_only: cmd += ['--merges'] cmd += ['--decorate'] if command.limit != 0: cmd += ['-%d' % command.limit] if not command.verbose: cmd += ['--pretty=short'] self._check_color(cmd) return self._run_command(cmd) def pull(self, _command): self._check_executable() cmd = [GitClient._executable, 'pull'] self._check_color(cmd) result = self._run_command(cmd) if result['returncode']: # check for detached HEAD cmd_rev_parse = [ GitClient._executable, 'rev-parse', '--abbrev-ref', 'HEAD'] result_rev_parse = self._run_command(cmd_rev_parse) if result_rev_parse['returncode']: result_rev_parse['output'] = 'Could not determine ref: ' + \ result_rev_parse['output'] return result_rev_parse detached = result_rev_parse['output'] == 'HEAD' if detached: # warn but not fail about the inability to pull a detached head return { 'cmd': '', 'cwd': self.path, 'output': result['output'], 'returncode': 0, } return result def push(self, _command): self._check_executable() cmd = [GitClient._executable, 'push'] return self._run_command(cmd) def remotes(self, _command): self._check_executable() cmd = [GitClient._executable, 'remote', '-v'] return self._run_command(cmd) def status(self, command): self._check_executable() while command.hide_empty: # check if ahead cmd = [GitClient._executable, 'log', '@{push}..'] result = self._run_command(cmd) if not result['returncode'] and result['output']: # ahead, do not hide break # check if behind cmd = [GitClient._executable, 'log', '..@{upstream}'] result = self._run_command(cmd) if not result['returncode'] and result['output']: # behind, do not hide break cmd = [GitClient._executable, 'status', '-s'] if command.quiet: cmd += ['--untracked-files=no'] result = self._run_command(cmd) if result['returncode'] or not result['output']: return result break cmd = [GitClient._executable, 'status'] self._check_color(cmd) if command.quiet: cmd += ['--untracked-files=no'] return self._run_command(cmd) def validate(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } self._check_executable() cmd_ls_remote = [GitClient._executable, 'ls-remote'] cmd_ls_remote += ['-q', '--exit-code'] cmd_ls_remote += [command.url] env = os.environ.copy() env['GIT_TERMINAL_PROMPT'] = '0' result_ls_remote = self._run_command( cmd_ls_remote, retry=command.retry, env=env) if result_ls_remote['returncode']: result_ls_remote['output'] = \ "Failed to contact remote repository '%s': %s" % \ (command.url, result_ls_remote['output']) return result_ls_remote if command.version: hashes = [] refs = [] tags = [] branches = [] for hash_and_ref in self._get_hash_ref_tuples( result_ls_remote['output'] ): hashes.append(hash_and_ref[0]) # ignore pull request refs if not hash_and_ref[1].startswith('refs/pull/'): if hash_and_ref[1].startswith('refs/tags/'): tags.append(hash_and_ref[1][10:]) elif hash_and_ref[1].startswith('refs/heads/'): branches.append(hash_and_ref[1][11:]) else: refs.append(hash_and_ref[1]) if command.version in refs: version_type = 'ref' version_name = command.version elif ( command.version.startswith('heads/') and command.version[6:] in branches ): version_type = 'branch' version_name = command.version[6:] elif ( command.version.startswith('tags/') and command.version[5:] in tags ): version_type = 'tag' version_name = command.version[5:] elif ( command.version in branches and command.version not in tags ): version_type = 'branch' version_name = command.version elif ( command.version in tags and command.version not in branches ): version_type = 'tag' version_name = command.version else: for _hash in hashes: if _hash.startswith(command.version): break else: cmd = result_ls_remote['cmd'] output = "Found git repository '%s' but " % command.url + \ 'unable to verify non-branch / non-tag ref ' + \ "'%s' without cloning the repo" % command.version return { 'cmd': cmd, 'cwd': self.path, 'output': output, 'returncode': 0 } cmd = result_ls_remote['cmd'] output = "Found git repository '%s' with %s '%s'" % \ (command.url, version_type, version_name) else: cmd = result_ls_remote['cmd'] output = "Found git repository '%s' with default branch" % \ command.url return { 'cmd': cmd, 'cwd': self.path, 'output': output, 'returncode': None } def _check_color(self, cmd): if not USE_COLOR: return # check if user uses colorization if GitClient._config_color_is_auto is None: _cmd = [GitClient._executable, 'config', '--get', 'color.ui'] result = self._run_command(_cmd) GitClient._config_color_is_auto = result['output'] in ['', 'auto'] # inject arguments to force colorization if GitClient._config_color_is_auto: cmd[1:1] = '-c', 'color.ui=always' def _check_executable(self): assert GitClient._executable is not None, \ "Could not find 'git' executable" def _get_hash_ref_tuples(self, ls_remote_output): tuples = [] for line in ls_remote_output.splitlines(): if line.startswith('#'): continue try: hash_, ref = line.split(None, 1) except ValueError: continue tuples.append((hash_, ref)) return tuples if not GitClient._executable: GitClient._executable = which('git') vcstool-0.3.0/vcstool/clients/hg.py000066400000000000000000000273021410413400700173030ustar00rootroot00000000000000import os from shutil import which from threading import Lock from vcstool.executor import USE_COLOR from .vcs_base import VcsClientBase from ..util import rmtree class HgClient(VcsClientBase): type = 'hg' _executable = None _config_color = None _config_color_lock = Lock() @staticmethod def is_repository(path): return os.path.isdir(os.path.join(path, '.hg')) def __init__(self, path): super(HgClient, self).__init__(path) def branch(self, command): self._check_executable() cmd = [HgClient._executable, 'branches' if command.all else 'branch'] self._check_color(cmd) return self._run_command(cmd) def custom(self, command): self._check_executable() cmd = [HgClient._executable] + command.args return self._run_command(cmd) def diff(self, command): self._check_executable() cmd = [HgClient._executable, 'diff'] self._check_color(cmd) if command.context: cmd += ['--unified %d' % command.context] return self._run_command(cmd) def export(self, command): self._check_executable() result_url = self._get_url() if result_url['returncode']: return result_url url = result_url['output'] cmd_id = [HgClient._executable, 'identify', '--id'] result_id = self._run_command(cmd_id) if result_id['returncode']: result_id['output'] = \ 'Could not determine id: ' + result_id['output'] return result_id id_ = result_id['output'] if not command.exact: cmd_branch = [HgClient._executable, 'identify', '--branch'] result_branch = self._run_command(cmd_branch) if result_branch['returncode']: result_branch['output'] = \ 'Could not determine branch: ' + result_branch['output'] return result_branch branch = result_branch['output'] cmd_branch_id = [ HgClient._executable, 'identify', '-r', branch, '--id'] result_branch_id = self._run_command(cmd_branch_id) if result_branch_id['returncode']: result_branch_id['output'] = \ 'Could not determine branch id: ' + \ result_branch_id['output'] return result_branch_id if result_branch_id['output'] == id_: id_ = branch cmd_branch = cmd_branch_id return { 'cmd': '%s && %s' % (result_url['cmd'], ' '.join(cmd_id)), 'cwd': self.path, 'output': '\n'.join([url, id_]), 'returncode': 0, 'export_data': {'url': url, 'version': id_} } def _get_url(self): cmd_url = [HgClient._executable, 'paths', 'default'] result_url = self._run_command(cmd_url) if result_url['returncode']: result_url['output'] = \ 'Could not determine url: ' + result_url['output'] return result_url return result_url def import_(self, command): if not command.url or not command.version: if not command.url and not command.version: value_missing = "'url' and 'version'" elif not command.url: value_missing = "'url'" else: value_missing = "'version'" return { 'cmd': '', 'cwd': self.path, 'output': 'Repository data lacks the %s value' % value_missing, 'returncode': 1 } self._check_executable() if HgClient.is_repository(self.path): # verify that existing repository is the same result_url = self._get_url() if result_url['returncode']: return result_url url = result_url['output'] if url != command.url: if not command.force: return { 'cmd': '', 'cwd': self.path, 'output': 'Path already exists and contains a different ' 'repository', 'returncode': 1 } try: rmtree(self.path) except OSError: os.remove(self.path) not_exist = self._create_path() if not_exist: return not_exist if HgClient.is_repository(self.path): # pull updates for existing repo cmd_pull = [ HgClient._executable, '--noninteractive', 'pull', '--update'] result_pull = self._run_command(cmd_pull, retry=command.retry) if result_pull['returncode']: return result_pull cmd = result_pull['cmd'] output = result_pull['output'] else: cmd_clone = [ HgClient._executable, '--noninteractive', 'clone', command.url, '.'] result_clone = self._run_command(cmd_clone, retry=command.retry) if result_clone['returncode']: result_clone['output'] = \ "Could not clone repository '%s': %s" % \ (command.url, result_clone['output']) return result_clone cmd = result_clone['cmd'] output = result_clone['output'] if command.version: cmd_checkout = [ HgClient._executable, '--noninteractive', 'checkout', command.version] result_checkout = self._run_command(cmd_checkout) if result_checkout['returncode']: result_checkout['output'] = \ "Could not checkout '%s': %s" % \ (command.version, result_checkout['output']) return result_checkout cmd += ' && ' + ' '.join(cmd_checkout) output = '\n'.join([output, result_checkout['output']]) return { 'cmd': cmd, 'cwd': self.path, 'output': output, 'returncode': 0 } def log(self, command): self._check_executable() if command.limit_tag: # check if specific tag exists cmd_log = [ HgClient._executable, 'log', '--rev', 'tag(%s)' % command.limit_tag] result_log = self._run_command(cmd_log) if result_log['returncode']: return { 'cmd': '', 'cwd': self.path, 'output': "Repository lacks the tag '%s'" % command.limit_tag, 'returncode': 1 } # output log since specific tag cmd = [ HgClient._executable, 'log', '--rev', 'sort(tag(%s)::, -rev) and not tag (%s)' % (command.limit_tag, command.limit_tag)] elif command.limit_untagged: # determine distance to nearest tag cmd_log = [ HgClient._executable, 'log', '--rev', '.', '--template', '{latesttagdistance}'] result_log = self._run_command(cmd_log) if result_log['returncode']: return result_log # output log since nearest tag cmd = [ HgClient._executable, 'log', '--limit', result_log['output'], '-b', '.'] else: cmd = [HgClient._executable, 'log'] if command.limit != 0: cmd += ['--limit', '%d' % command.limit] if command.verbose: cmd += ['--verbose'] self._check_color(cmd) return self._run_command(cmd) def pull(self, _command): self._check_executable() cmd = [HgClient._executable, '--noninteractive', 'pull', '--update'] self._check_color(cmd) return self._run_command(cmd) def push(self, _command): self._check_executable() cmd = [HgClient._executable, '--noninteractive', 'push'] return self._run_command(cmd) def remotes(self, _command): self._check_executable() cmd = [HgClient._executable, 'paths'] return self._run_command(cmd) def status(self, command): self._check_executable() cmd = [HgClient._executable, 'status'] self._check_color(cmd) if command.quiet: cmd += ['--untracked-files=no'] return self._run_command(cmd) def validate(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } self._check_executable() cmd_id_repo = [ HgClient._executable, '--noninteractive', 'identify', command.url] result_id_repo = self._run_command( cmd_id_repo, retry=command.retry) if result_id_repo['returncode']: result_id_repo['output'] = \ "Failed to contact remote repository '%s': %s" % \ (command.url, result_id_repo['output']) return result_id_repo if command.version: cmd_id_ver = [ HgClient._executable, '--noninteractive', 'identify', '-r', command.version, command.url] result_id_ver = self._run_command( cmd_id_ver, retry=command.retry) if result_id_ver['returncode']: result_id_ver['output'] = \ 'Specified version not found on remote repository ' + \ "'%s':'%s' : %s" % \ (command.url, command.version, result_id_ver['output']) return result_id_ver cmd = result_id_ver['cmd'] output = "Found hg repository '%s' with changeset '%s'" % \ (command.url, command.version) else: cmd = result_id_repo['cmd'] output = "Found hg repository '%s' with default branch" % \ command.url return { 'cmd': cmd, 'cwd': self.path, 'output': output, 'returncode': None } def _check_color(self, cmd): if not USE_COLOR: return with HgClient._config_color_lock: # check if user uses colorization if HgClient._config_color is None: HgClient._config_color = False # check if config extension is available _cmd = [HgClient._executable, 'config', '--help'] result = self._run_command(_cmd) if result['returncode']: return # check if color extension is available and not disabled _cmd = [HgClient._executable, 'config', 'extensions.color'] result = self._run_command(_cmd) if result['returncode'] or result['output'].startswith('!'): return # check if color mode is not off or not set _cmd = [HgClient._executable, 'config', 'color.mode'] result = self._run_command(_cmd) if not result['returncode'] and result['output'] == 'off': return HgClient._config_color = True # inject arguments to force colorization if HgClient._config_color: cmd[1:1] = '--color', 'always' def _check_executable(self): assert HgClient._executable is not None, \ "Could not find 'hg' executable" if not HgClient._executable: HgClient._executable = which('hg') vcstool-0.3.0/vcstool/clients/none.py000066400000000000000000000002501410413400700176350ustar00rootroot00000000000000from .vcs_base import VcsClientBase class NoneClient(VcsClientBase): type = 'none' def __init__(self, path): super(NoneClient, self).__init__(path) vcstool-0.3.0/vcstool/clients/svn.py000066400000000000000000000205171410413400700175140ustar00rootroot00000000000000import os from shutil import which from xml.etree.ElementTree import fromstring from .vcs_base import VcsClientBase class SvnClient(VcsClientBase): type = 'svn' _executable = None @staticmethod def is_repository(path): return os.path.isdir(os.path.join(path, '.svn')) def __init__(self, path): super(SvnClient, self).__init__(path) def branch(self, command): if command.all: return self._not_applicable( command, message='at least with the option to list all branches') self._check_executable() cmd_info = [SvnClient._executable, 'info', '--xml'] result_info = self._run_command(cmd_info) if result_info['returncode']: result_info['output'] = \ 'Could not determine url: ' + result_info['output'] return result_info info = result_info['output'] try: root = fromstring(info) entry = root.find('entry') url = entry.findtext('url') repository = entry.find('repository') root_url = repository.findtext('root') except Exception as e: return { 'cmd': '', 'cwd': self.path, 'output': 'Could not determine url from xml: %s' % e, 'returncode': 1 } if not url.startswith(root_url): return { 'cmd': '', 'cwd': self.path, 'output': "Could not determine url suffix. The root url '%s' is not " "a prefix of the url '%s'" % (root_url, url), 'returncode': 1 } return { 'cmd': ' '.join(cmd_info), 'cwd': self.path, 'output': url[len(root_url):], 'returncode': 0, } def custom(self, command): self._check_executable() cmd = [SvnClient._executable] + command.args return self._run_command(cmd) def diff(self, command): self._check_executable() cmd = [SvnClient._executable, 'diff'] if command.context: cmd += ['--unified=%d' % command.context] return self._run_command(cmd) def export(self, command): self._check_executable() cmd_info = [SvnClient._executable, 'info', '--xml'] result_info = self._run_command(cmd_info) if result_info['returncode']: result_info['output'] = \ 'Could not determine url: ' + result_info['output'] return result_info info = result_info['output'] try: root = fromstring(info) entry = root.find('entry') url = entry.findtext('url') revision = entry.get('revision') except Exception as e: return { 'cmd': '', 'cwd': self.path, 'output': 'Could not determine url from xml: %s' % e, 'returncode': 1 } export_data = {'url': url} if command.exact: export_data['version'] = revision return { 'cmd': ' '.join(cmd_info), 'cwd': self.path, 'output': url, 'returncode': 0, 'export_data': export_data } def import_(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } not_exist = self._create_path() if not_exist: return not_exist self._check_executable() url = command.url if command.version: url += '@%s' % command.version cmd_checkout = [ SvnClient._executable, '--non-interactive', 'checkout', url, '.'] result_checkout = self._run_command(cmd_checkout, retry=command.retry) if result_checkout['returncode']: result_checkout['output'] = \ "Could not checkout repository '%s': %s" % \ (command.url, result_checkout['output']) return result_checkout return { 'cmd': ' '.join(cmd_checkout), 'cwd': self.path, 'output': result_checkout['output'], 'returncode': 0 } def log(self, command): if command.limit_tag: return { 'cmd': '', 'cwd': self.path, 'output': 'SvnClient can not determine log since tag', 'returncode': NotImplemented } if command.limit_untagged: return { 'cmd': '', 'cwd': self.path, 'output': 'SvnClient can not determine latest tag', 'returncode': NotImplemented } self._check_executable() cmd = [SvnClient._executable, 'log'] if command.limit != 0: cmd += ['--limit', '%d' % command.limit] return self._run_command(cmd) def pull(self, _command): self._check_executable() cmd = [SvnClient._executable, '--non-interactive', 'update'] return self._run_command(cmd) def push(self, command): self._check_executable() return self._not_applicable(command) def remotes(self, _command): self._check_executable() cmd_info = [SvnClient._executable, 'info', '--xml'] result_info = self._run_command(cmd_info) if result_info['returncode']: result_info['output'] = \ 'Could not determine url: ' + result_info['output'] return result_info info = result_info['output'] try: root = fromstring(info) entry = root.find('entry') url = entry.findtext('url') except Exception as e: return { 'cmd': '', 'cwd': self.path, 'output': 'Could not determine url from xml: %s' % e, 'returncode': 1 } return { 'cmd': ' '.join(cmd_info), 'cwd': self.path, 'output': url, 'returncode': 0, } def status(self, command): self._check_executable() cmd = [SvnClient._executable, 'status'] if command.quiet: cmd += ['--quiet'] return self._run_command(cmd) def validate(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } self._check_executable() cmd_info_repo = [SvnClient._executable, 'info', command.url] result_info_repo = self._run_command( cmd_info_repo, retry=command.retry) if result_info_repo['returncode']: result_info_repo['output'] = \ "Failed to contact remote repository '%s': %s" % \ (command.url, result_info_repo['output']) return result_info_repo if command.version: cmd_info_ver = [ SvnClient._executable, 'info', command.url + '@' + command.version] result_info_ver = self._run_command( cmd_info_ver, retry=command.retry) if result_info_ver['returncode']: result_info_ver['output'] = \ 'Specified version not found on remote repository' + \ "'%s@%s' : %s" % \ (command.url, command.version, result_info_ver['output']) return result_info_ver cmd = result_info_ver['cmd'] output = "Found svn repository '%s' with revision '%s'" % \ (command.url, command.version) else: cmd = result_info_repo['cmd'] output = "Found svn repository '%s' with default branch" % \ command.url return { 'cmd': cmd, 'cwd': self.path, 'output': output, 'returncode': None } def _check_executable(self): assert SvnClient._executable is not None, \ "Could not find 'svn' executable" if not SvnClient._executable: SvnClient._executable = which('svn') vcstool-0.3.0/vcstool/clients/tar.py000066400000000000000000000066531410413400700175010ustar00rootroot00000000000000from io import BytesIO import os import tarfile from urllib.error import URLError from .vcs_base import load_url from .vcs_base import test_url from .vcs_base import VcsClientBase from ..util import rmtree class TarClient(VcsClientBase): type = 'tar' @staticmethod def is_repository(path): return False def __init__(self, path): super(TarClient, self).__init__(path) def import_(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } # clear destination if os.path.exists(self.path): for filename in os.listdir(self.path): path = os.path.join(self.path, filename) try: rmtree(path) except OSError: os.remove(path) else: not_exist = self._create_path() if not_exist: return not_exist # download tarball try: data = load_url(command.url, retry=command.retry) except URLError as e: return { 'cmd': '', 'cwd': self.path, 'output': "Could not fetch tarball from '%s': %s" % (command.url, e), 'returncode': 1 } # unpack tarball into destination try: # raise all fatal errors tar = tarfile.open(mode='r', fileobj=BytesIO(data), errorlevel=1) except (tarfile.ReadError, IOError, OSError) as e: return { 'cmd': '', 'cwd': self.path, 'output': "Failed to read tarball fetched from '%s': %s" % (command.url, e), 'returncode': 1 } if not command.version: members = None else: # remap all members from version subfolder into destination def get_members(tar, prefix): for tar_info in tar.getmembers(): if tar_info.name.startswith(prefix): tar_info.name = tar_info.name[len(prefix):] yield tar_info prefix = str(command.version) + '/' members = get_members(tar, prefix) tar.extractall(self.path, members) return { 'cmd': '', 'cwd': self.path, 'output': "Downloaded tarball from '%s' and unpacked it" % command.url, 'returncode': 0 } def validate(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } # test url try: test_url(command.url, retry=command.retry) except URLError as e: return { 'cmd': '', 'cwd': self.path, 'output': "Failed to contact tarball url '%s': %s" % (command.url, e), 'returncode': 1 } return { 'cmd': 'http HEAD url', 'cwd': self.path, 'output': "Tarball url '%s' exists" % command.url, 'returncode': None } vcstool-0.3.0/vcstool/clients/vcs_base.py000066400000000000000000000075371410413400700205020ustar00rootroot00000000000000import os import socket import subprocess import time from urllib.error import HTTPError from urllib.error import URLError from urllib.request import Request from urllib.request import urlopen class VcsClientBase(object): type = None def __init__(self, path): self.path = path def __getattribute__(self, name): if name == 'import': try: return self.import_ except AttributeError: pass return super(VcsClientBase, self).__getattribute__(name) def _not_applicable(self, command, message=None): return { 'cmd': '%s.%s(%s)' % ( self.__class__.type, 'push', command.__class__.command), 'output': "Command '%s' not applicable for client '%s'%s" % ( command.__class__.command, self.__class__.type, ': ' + message if message else ''), 'returncode': NotImplemented } def _run_command(self, cmd, env=None, retry=0): for i in range(retry + 1): result = run_command(cmd, os.path.abspath(self.path), env=env) if not result['returncode']: # return successful result break if i >= retry: # return the failure after retries break # increasing sleep before each retry time.sleep(i + 1) return result def _create_path(self): if not os.path.exists(self.path): try: os.makedirs(self.path) except os.error as e: return { 'cmd': 'os.makedirs(%s)' % self.path, 'cwd': self.path, 'output': "Could not create directory '%s': %s" % (self.path, e), 'returncode': 1 } return None def run_command(cmd, cwd, env=None): if not os.path.exists(cwd): cwd = None result = {'cmd': ' '.join(cmd), 'cwd': cwd} try: proc = subprocess.Popen( cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) output, _ = proc.communicate() result['output'] = output.rstrip().decode('utf8') result['returncode'] = proc.returncode except subprocess.CalledProcessError as e: result['output'] = e.output.decode('utf8') result['returncode'] = e.returncode return result def load_url(url, retry=2, retry_period=1, timeout=10): try: fh = urlopen(url, timeout=timeout) except HTTPError as e: if e.code == 503 and retry: time.sleep(retry_period) return load_url( url, retry=retry - 1, retry_period=retry_period, timeout=timeout) e.msg += ' (%s)' % url raise except URLError as e: if isinstance(e.reason, socket.timeout) and retry: time.sleep(retry_period) return load_url( url, retry=retry - 1, retry_period=retry_period, timeout=timeout) raise URLError(str(e) + ' (%s)' % url) return fh.read() def test_url(url, retry=2, retry_period=1, timeout=10): request = Request(url) request.get_method = lambda: 'HEAD' try: response = urlopen(request) except HTTPError as e: if e.code == 503 and retry: time.sleep(retry_period) return test_url( url, retry=retry - 1, retry_period=retry_period, timeout=timeout) e.msg += ' (%s)' % url raise except URLError as e: if isinstance(e.reason, socket.timeout) and retry: time.sleep(retry_period) return test_url( url, retry=retry - 1, retry_period=retry_period, timeout=timeout) raise URLError(str(e) + ' (%s)' % url) return response vcstool-0.3.0/vcstool/clients/zip.py000066400000000000000000000104441410413400700175060ustar00rootroot00000000000000from io import BytesIO import os from urllib.error import URLError import zipfile from .vcs_base import load_url from .vcs_base import test_url from .vcs_base import VcsClientBase from ..util import rmtree class ZipClient(VcsClientBase): type = 'zip' @staticmethod def is_repository(path): return False def __init__(self, path): super(ZipClient, self).__init__(path) def import_(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } # clear destination if os.path.exists(self.path): for filename in os.listdir(self.path): path = os.path.join(self.path, filename) try: rmtree(path) except OSError: os.remove(path) else: not_exist = self._create_path() if not_exist: return not_exist # download zipfile try: data = load_url(command.url, retry=command.retry) except URLError as e: return { 'cmd': '', 'cwd': self.path, 'output': "Could not fetch zipfile from '%s': %s" % (command.url, e), 'returncode': 1 } def create_path(path): if not os.path.exists(path): try: os.makedirs(path) except os.error as e: return { 'cmd': 'os.makedirs(%s)' % path, 'cwd': path, 'output': "Could not create directory '%s': %s" % (path, e), 'returncode': 1 } return None # unpack zipfile into destination try: zip_file = zipfile.ZipFile(BytesIO(data), mode='r') except zipfile.BadZipfile as e: return { 'cmd': 'ZipFile(%s)' % command.url, 'cwd': self.path, 'output': "Could not read zipfile from '%s': %s" % (command.url, e), 'returncode': 1 } try: if not command.version: zip_file.extractall(self.path) else: prefix = str(command.version) + '/' for name in zip_file.namelist(): if name.startswith(prefix): if not name[len(prefix):]: continue # remap members from version subfolder into destination dst = os.path.join(self.path, name[len(prefix):]) if dst.endswith('/'): # create directories not_exist = create_path(dst) if not_exist: return not_exist else: with zip_file.open(name, mode='r') as src_handle: with open(dst, 'wb') as dst_handle: dst_handle.write(src_handle.read()) finally: zip_file.close() return { 'cmd': '', 'cwd': self.path, 'output': "Downloaded zipfile from '%s' and unpacked it" % command.url, 'returncode': 0 } def validate(self, command): if not command.url: return { 'cmd': '', 'cwd': self.path, 'output': "Repository data lacks the 'url' value", 'returncode': 1 } # test url try: test_url(command.url, retry=command.retry) except URLError as e: return { 'cmd': '', 'cwd': self.path, 'output': "Failed to contact zip url '%s': %s" % (command.url, e), 'returncode': 1 } return { 'cmd': 'http HEAD', 'cwd': self.path, 'output': "Zip url '%s' exists" % command.url, 'returncode': None } vcstool-0.3.0/vcstool/commands/000077500000000000000000000000001410413400700164675ustar00rootroot00000000000000vcstool-0.3.0/vcstool/commands/__init__.py000066400000000000000000000020001410413400700205700ustar00rootroot00000000000000from .branch import BranchCommand from .custom import CustomCommand from .diff import DiffCommand from .export import ExportCommand from .import_ import ImportCommand from .log import LogCommand from .pull import PullCommand from .push import PushCommand from .remotes import RemotesCommand from .status import StatusCommand from .validate import ValidateCommand vcstool_commands = [] vcstool_commands.append(BranchCommand) vcstool_commands.append(CustomCommand) vcstool_commands.append(DiffCommand) vcstool_commands.append(ExportCommand) vcstool_commands.append(ImportCommand) vcstool_commands.append(LogCommand) vcstool_commands.append(PullCommand) vcstool_commands.append(PushCommand) vcstool_commands.append(RemotesCommand) vcstool_commands.append(StatusCommand) vcstool_commands.append(ValidateCommand) _commands = [c.command for c in vcstool_commands] if len(_commands) != len(set(_commands)): raise RuntimeError( 'Multiple commands share the same command name: ' + ', '.join(sorted(_commands))) vcstool-0.3.0/vcstool/commands/branch.py000066400000000000000000000015401410413400700202760ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class BranchCommand(Command): command = 'branch' help = 'Show the branches' def __init__(self, args): super(BranchCommand, self).__init__(args) self.all = args.all def get_parser(): parser = argparse.ArgumentParser( description='Show the current branch', prog='vcs branch') group = parser.add_argument_group('"branch" command parameters') group.add_argument( '--all', action='store_true', default=False, help='Show all branches') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, BranchCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/command.py000066400000000000000000000067271410413400700204730ustar00rootroot00000000000000import argparse from multiprocessing import cpu_count import os from vcstool.crawler import find_repositories from vcstool.executor import execute_jobs from vcstool.executor import generate_jobs from vcstool.executor import output_repositories from vcstool.executor import output_results class Command(object): command = None def __init__(self, args): self.debug = args.debug if 'debug' in args else False self.hide_empty = args.hide_empty if 'hide_empty' in args else False self.nested = args.nested if 'nested' in args else False self.output_repos = args.repos if 'repos' in args else False if 'paths' in args: self.paths = args.paths else: self.paths = [args.path] def check_greater_zero(value): try: value = int(value) except ValueError: raise argparse.ArgumentTypeError("invalid int value: '%s'" % value) if value <= 0: raise argparse.ArgumentTypeError( "invalid positive int value: '%d'" % value) return value def add_common_arguments( parser, skip_hide_empty=False, skip_nested=False, path_nargs='*', path_help=None ): parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter group = parser.add_argument_group('Common parameters') group.add_argument( '--debug', action='store_true', default=False, help='Show debug messages') if not skip_hide_empty: group.add_argument( '-s', '--hide-empty', '--skip-empty', action='store_true', default=False, help='Hide repositories with empty output') if not skip_nested: group.add_argument( '-n', '--nested', action='store_true', default=False, help='Search for nested repositories') try: default_workers = cpu_count() except NotImplementedError: default_workers = 4 group.add_argument( '-w', '--workers', type=check_greater_zero, metavar='N', default=default_workers, help='Number of parallel worker threads') group.add_argument( '--repos', action='store_true', default=False, help='List repositories which the command operates on') if path_nargs == '?': path_help = path_help or 'Base path to look for repositories' group.add_argument( 'path', nargs=path_nargs, type=existing_dir, default=os.curdir, help=path_help) elif path_nargs == '*': path_help = path_help or 'Base paths to look for repositories' group.add_argument( 'paths', nargs=path_nargs, type=existing_dir, default=[os.curdir], help=path_help) def existing_dir(path): if not os.path.exists(path): raise argparse.ArgumentTypeError("Path '%s' does not exist." % path) if not os.path.isdir(path): raise argparse.ArgumentTypeError( "Path '%s' is not a directory." % path) return path def simple_main(parser, command_class, args=None): add_common_arguments(parser) args = parser.parse_args(args) command = command_class(args) clients = find_repositories(command.paths, nested=command.nested) if command.output_repos: output_repositories(clients) jobs = generate_jobs(clients, command) results = execute_jobs( jobs, show_progress=True, number_of_workers=args.workers, debug_jobs=args.debug) output_results(results, hide_empty=args.hide_empty) any_error = any(r['returncode'] for r in results) return 1 if any_error else 0 vcstool-0.3.0/vcstool/commands/custom.py000066400000000000000000000067251410413400700203650ustar00rootroot00000000000000import argparse import sys from vcstool.clients import vcstool_clients from vcstool.crawler import find_repositories from vcstool.executor import execute_jobs from vcstool.executor import generate_jobs from vcstool.executor import output_repositories from vcstool.executor import output_results from vcstool.streams import set_streams from .command import add_common_arguments from .command import Command class CustomCommand(Command): command = 'custom' help = 'Run a custom command' def __init__(self, args): super(CustomCommand, self).__init__(args) self.args = args.args def get_parser(): parser = argparse.ArgumentParser( description='Run a custom command', prog='vcs custom') group = parser.add_argument_group( '"custom" command parameters restricting the repositories') for client_type in [ c.type for c in vcstool_clients if c.type not in ['tar'] ]: group.add_argument( '--' + client_type, action='store_true', default=False, help="Run command on '%s' repositories" % client_type) group = parser.add_argument_group('"custom" command parameters') group.add_argument( '--args', required=True, nargs='*', help='Arbitrary arguments passed ' 'to each vcs invocation. It must be passed after other arguments ' 'since it collects all following options.') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() add_common_arguments(parser) # separate anything followed after --args to not confuse argparse if args is None: args = sys.argv[1:] try: index = args.index('--args') + 1 except ValueError: # should generate error due to missing --args parser.parse_known_args(args) client_args = args[index:] args = parser.parse_args(args[0:index]) args.args = client_args # check if any client type is specified any_client_type = False for client in vcstool_clients: if client.type in args and args.__dict__[client.type]: any_client_type = True break # if no client type is specified enable all client types if not any_client_type: for client in vcstool_clients: if client.type in args: args.__dict__[client.type] = True command = CustomCommand(args) # filter repositories by specified client types clients = find_repositories(command.paths, nested=command.nested) clients = [c for c in clients if c.type in args and args.__dict__[c.type]] if command.output_repos: output_repositories(clients) jobs = generate_jobs(clients, command) results = execute_jobs( jobs, show_progress=True, number_of_workers=args.workers, debug_jobs=args.debug) output_results(results, hide_empty=args.hide_empty) any_error = any(r['returncode'] for r in results) return 1 if any_error else 0 def bzr_main(args=None): if args is None: args = sys.argv[1:] return main(['--bzr', '--args'] + args) def git_main(args=None): if args is None: args = sys.argv[1:] return main(['--git', '--args'] + args) def hg_main(args=None): if args is None: args = sys.argv[1:] return main(['--hg', '--args'] + args) def svn_main(args=None): if args is None: args = sys.argv[1:] return main(['--svn', '--args'] + args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/diff.py000066400000000000000000000016121410413400700177510ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class DiffCommand(Command): command = 'diff' help = 'Show changes in the working tree' def __init__(self, args): super(DiffCommand, self).__init__(args) self.context = args.context def get_parser(): parser = argparse.ArgumentParser( description='Show changes in the working tree', prog='vcs diff') group = parser.add_argument_group('"diff" command parameters') group.add_argument( '--context', metavar='N', type=int, help='Generate diffs with lines of context') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, DiffCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/export.py000066400000000000000000000100271410413400700203620ustar00rootroot00000000000000import argparse import os import sys from vcstool.crawler import find_repositories from vcstool.executor import ansi from vcstool.executor import execute_jobs from vcstool.executor import generate_jobs from vcstool.executor import output_repositories from vcstool.executor import output_results from vcstool.streams import set_streams from .command import add_common_arguments from .command import Command class ExportCommand(Command): command = 'export' help = 'Export the list of repositories' def __init__(self, args): super(ExportCommand, self).__init__(args) self.exact = args.exact or args.exact_with_tags self.with_tags = args.exact_with_tags def get_parser(): parser = argparse.ArgumentParser( description='Export the list of repositories', prog='vcs export') group = parser.add_argument_group('"export" command parameters') group_exact = group.add_mutually_exclusive_group() group_exact.add_argument( '--exact', action='store_true', default=False, help='Export commit hashes instead of branch names') group_exact.add_argument( '--exact-with-tags', action='store_true', default=False, help='Export unique tag names or commit hashes instead of branch ' 'names') return parser def output_export_data(result, hide_empty=False): # errors are handled by a separate function if result['returncode']: return try: lines = [] lines.append(' %s:' % result['path']) lines.append(' type: ' + result['client'].__class__.type) export_data = result['export_data'] lines.append(' url: ' + export_data['url']) if 'version' in export_data and export_data['version']: lines.append(' version: ' + export_data['version']) print('\n'.join(lines)) except KeyError as e: print( ansi('redf') + ( "Command '%s' failed for path '%s': %s: %s" % ( result['command'].__class__.command, result['client'].path, e.__class__.__name__, e)) + ansi('reset'), file=sys.stderr) def output_error_information(result, hide_empty=False): # successful results are handled by a separate function if not result['returncode']: return if result['returncode'] == NotImplemented: color = 'yellow' else: color = 'red' line = '%s: %s' % (result['path'], result['output']) print(ansi('%sf' % color) + line + ansi('reset'), file=sys.stderr) def get_relative_path_of_result(result): client = result['client'] return os.path.relpath(client.path, result['command'].paths[0]) def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() add_common_arguments(parser, skip_hide_empty=True, path_nargs='?') args = parser.parse_args(args) command = ExportCommand(args) clients = find_repositories(command.paths, nested=command.nested) if command.output_repos: output_repositories(clients) jobs = generate_jobs(clients, command) results = execute_jobs(jobs, number_of_workers=args.workers) # check if at least one repo was found in the client directory basename = None for result in results: result['path'] = get_relative_path_of_result(result) if result['path'] == '.': basename = os.path.basename(os.path.abspath(result['client'].path)) # in that case prefix all relative paths with the client directory basename if basename is not None: for result in results: if result['path'] == '.': result['path'] = basename else: result['path'] = os.path.join(basename, result['path']) print('repositories:') output_results(results, output_handler=output_export_data) output_results(results, output_handler=output_error_information) any_error = any(r['returncode'] for r in results) return 1 if any_error else 0 if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/help.py000066400000000000000000000102101410413400700177630ustar00rootroot00000000000000import argparse import sys from pkg_resources import load_entry_point from vcstool.clients import vcstool_clients from vcstool.commands import vcstool_commands from vcstool.streams import set_streams def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) # no help to extract command first (which might be followed by --help) parser = get_parser(add_help=False) ns, _ = parser.parse_known_args(args) # help for a specific command if ns.command: # relay help request foe specific command entrypoint = get_entrypoint(ns.command) if not entrypoint: return 1 return entrypoint(['--help']) # regular parsing validating options and arguments parser = get_parser() ns = parser.parse_args(args) if ns.clients: print('The available VCS clients are:') for client in vcstool_clients: print(' ' + client.type) return 0 if ns.commands: print(' '.join([cmd.command for cmd in vcstool_commands])) return 0 if ns.commands_descriptions: print('\n'.join(['{}\t{}'.format(cmd.command, cmd.help) for cmd in vcstool_commands])) return 0 # output detailed command list parser = get_parser_with_command_only() parser.print_help() return 0 def get_parser(add_help=True): parser = argparse.ArgumentParser( prog='vcs', description=_get_description(), epilog=_get_epilog(), add_help=add_help) group = parser.add_mutually_exclusive_group() group.add_argument( 'command', metavar='', nargs='?', help='The available commands: ' + ', '.join( [cmd.command for cmd in vcstool_commands])) group.add_argument( '--clients', action='store_true', default=False, help='Show the available VCS clients') group.add_argument( '--commands', action='store_true', default=False, help='Output the available commands for auto-completion') group.add_argument( '--commands-descriptions', action='store_true', default=False, help='Output the available commands along with their descriptions') from vcstool import __version__ group.add_argument( '--version', action='version', version='%(prog)s ' + __version__, help='Show the vcstool version') return parser def get_entrypoint(command): # accept command with same prefix if unique commands = [cmd.command for cmd in vcstool_commands] commands = [cmd for cmd in commands if cmd.startswith(command)] if len(commands) != 1: print( "vcs: '%s' is not a vcs command. See 'vcs help'." % command, file=sys.stderr) if commands: print( '\nDid you mean one of these?\n' + '\n '.join(commands), file=sys.stderr) return None return load_entry_point( 'vcstool', 'console_scripts', 'vcs-' + commands[0]) def get_parser_with_command_only(): parser = argparse.ArgumentParser( prog='vcs', usage='%(prog)s ', formatter_class=argparse.RawDescriptionHelpFormatter, description='%s\n\n%s' % ( _get_description(), '\n'.join(_get_command_help(vcstool_commands))), epilog=_get_epilog(), add_help=False) parser.add_argument('command', help=argparse.SUPPRESS) return parser def _get_description(): return 'Most commands take directory arguments, ' \ 'recursively searching for repositories\n' \ 'in these directories. ' \ 'If no arguments are supplied to a command, it recurses\n' \ 'on the current directory (inclusive) by default.' def _get_epilog(): return "See '%(prog)s --help' for more information " \ 'on a specific command.' def _get_command_help(commands): lines = ['The available commands are:'] max_len = max(len(cmd.command) for cmd in commands) for cmd in vcstool_commands: lines.append( ' %s%s %s' % (cmd.command, ' ' * (max_len - len(cmd.command)), cmd.help)) return lines if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/import_.py000066400000000000000000000211371410413400700205160ustar00rootroot00000000000000import argparse import os from shutil import which import sys import urllib.request as request from vcstool import __version__ as vcstool_version from vcstool.clients import vcstool_clients from vcstool.clients.vcs_base import run_command from vcstool.executor import ansi from vcstool.executor import execute_jobs from vcstool.executor import output_repositories from vcstool.executor import output_results from vcstool.streams import set_streams import yaml from .command import add_common_arguments from .command import Command class ImportCommand(Command): command = 'import' help = 'Import the list of repositories' def __init__( self, args, url, version=None, recursive=False, shallow=False ): super(ImportCommand, self).__init__(args) self.url = url self.version = version self.force = args.force self.retry = args.retry self.skip_existing = args.skip_existing self.recursive = recursive self.shallow = shallow def get_parser(): parser = argparse.ArgumentParser( description='Import the list of repositories', prog='vcs import') group = parser.add_argument_group('"import" command parameters') group.add_argument( '--input', type=file_or_url_type, default='-', help='Where to read YAML from', metavar='FILE_OR_URL') group.add_argument( '--force', action='store_true', default=False, help="Delete existing directories if they don't contain the " 'repository being imported') group.add_argument( '--shallow', action='store_true', default=False, help='Create a shallow clone without a history') group.add_argument( '--recursive', action='store_true', default=False, help='Recurse into submodules') group.add_argument( '--retry', type=int, metavar='N', default=2, help='Retry commands requiring network access N times on failure') group.add_argument( '--skip-existing', action='store_true', default=False, help="Don't overwrite existing directories or change custom checkouts " 'in repos using the same URL (but fetch repos with same URL)') return parser def file_or_url_type(value): if os.path.exists(value) or '://' not in value: return argparse.FileType('r')(value) # use another user agent to avoid getting a 403 (forbidden) error, # since some websites blacklist or block unrecognized user agents return request.Request( value, headers={'User-Agent': 'vcstool/' + vcstool_version}) def get_repositories(yaml_file): try: root = yaml.safe_load(yaml_file) except yaml.YAMLError as e: raise RuntimeError('Input data is not valid yaml format: %s' % e) try: repositories = root['repositories'] return get_repos_in_vcstool_format(repositories) except KeyError as e: raise RuntimeError('Input data is not valid format: %s' % e) except TypeError as e: # try rosinstall file format try: return get_repos_in_rosinstall_format(root) except Exception: raise RuntimeError('Input data is not valid format: %s' % e) def get_repos_in_vcstool_format(repositories): repos = {} if repositories is None: print( ansi('yellowf') + 'List of repositories is empty' + ansi('reset'), file=sys.stderr) return repos for path in repositories: repo = {} attributes = repositories[path] try: repo['type'] = attributes['type'] repo['url'] = attributes['url'] if 'version' in attributes: repo['version'] = attributes['version'] except KeyError as e: print( ansi('yellowf') + ( "Repository '%s' does not provide the necessary " 'information: %s' % (path, e)) + ansi('reset'), file=sys.stderr) continue repos[path] = repo return repos def get_repos_in_rosinstall_format(root): repos = {} for i, item in enumerate(root): if len(item.keys()) != 1: raise RuntimeError('Input data is not valid format') repo = {'type': list(item.keys())[0]} attributes = list(item.values())[0] try: path = attributes['local-name'] except KeyError as e: print( ansi('yellowf') + ( 'Repository #%d does not provide the necessary ' 'information: %s' % (i, e)) + ansi('reset'), file=sys.stderr) continue try: repo['url'] = attributes['uri'] if 'version' in attributes: repo['version'] = attributes['version'] except KeyError as e: print( ansi('yellowf') + ( "Repository '%s' does not provide the necessary " 'information: %s' % (path, e)) + ansi('reset'), file=sys.stderr) continue repos[path] = repo return repos def generate_jobs(repos, args): jobs = [] for path, repo in repos.items(): path = os.path.join(args.path, path) clients = [c for c in vcstool_clients if c.type == repo['type']] if not clients: from vcstool.clients.none import NoneClient job = { 'client': NoneClient(path), 'command': None, 'cwd': path, 'output': "Repository type '%s' is not supported" % repo['type'], 'returncode': NotImplemented } jobs.append(job) continue client = clients[0](path) command = ImportCommand( args, repo['url'], str(repo['version']) if 'version' in repo else None, recursive=args.recursive, shallow=args.shallow) job = {'client': client, 'command': command} jobs.append(job) return jobs def add_dependencies(jobs): paths = [job['client'].path for job in jobs] for job in jobs: job['depends'] = set() path = job['client'].path while True: parent_path = os.path.dirname(path) if parent_path == path: break path = parent_path if path in paths: job['depends'].add(path) def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() add_common_arguments( parser, skip_hide_empty=True, skip_nested=True, path_nargs='?', path_help='Base path to clone repositories to') args = parser.parse_args(args) try: input_ = args.input if isinstance(input_, request.Request): input_ = request.urlopen(input_) repos = get_repositories(input_) except (RuntimeError, request.URLError) as e: print(ansi('redf') + str(e) + ansi('reset'), file=sys.stderr) return 1 jobs = generate_jobs(repos, args) add_dependencies(jobs) if args.repos: output_repositories([job['client'] for job in jobs]) workers = args.workers # for ssh URLs check if the host is known to prevent ssh asking for # confirmation when using more than one worker if workers > 1: ssh_keygen = None checked_hosts = set() for job in list(jobs): if job['command'] is None: continue url = job['command'].url # only check the host from a ssh URL if not url.startswith('git@') or ':' not in url: continue host = url[4:].split(':', 1)[0] # only check each host name once if host in checked_hosts: continue checked_hosts.add(host) # get ssh-keygen path once if ssh_keygen is None: ssh_keygen = which('ssh-keygen') or False if not ssh_keygen: continue result = run_command([ssh_keygen, '-F', host], '') if result['returncode']: print( 'At least one hostname (%s) is unknown, switching to a ' 'single worker to allow interactively answering the ssh ' 'question to confirm the fingerprint' % host) workers = 1 break results = execute_jobs( jobs, show_progress=True, number_of_workers=workers, debug_jobs=args.debug) output_results(results) any_error = any(r['returncode'] for r in results) return 1 if any_error else 0 if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/log.py000066400000000000000000000031621410413400700176240ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class LogCommand(Command): command = 'log' help = 'Show commit logs' def __init__(self, args): super(LogCommand, self).__init__(args) self.limit = args.limit self.limit_tag = args.limit_tag self.limit_untagged = args.limit_untagged self.merge_only = args.merge_only self.verbose = args.verbose def get_parser(): parser = argparse.ArgumentParser( description='Show commit logs', prog='vcs log') group = parser.add_argument_group('"log" command parameters') group.add_argument( '-l', '--limit', metavar='N', type=int, default=3, help='Limit number of logs (0 for unlimited)') ex_group = group.add_mutually_exclusive_group() ex_group.add_argument( '--limit-tag', metavar='TAG', help='Limit number of log from the head to the specified tag') ex_group.add_argument( '--limit-untagged', action='store_true', default=False, help='Limit number of log from the head to the last tagged commit') group.add_argument( '--merge-only', action='store_true', default=False, help='Show only merge commits') group.add_argument( '--verbose', action='store_true', default=False, help='Show the full commit message') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, LogCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/pull.py000066400000000000000000000014271410413400700200210ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class PullCommand(Command): command = 'pull' help = 'Bring changes from the repository into the working copy' def __init__(self, args): super(PullCommand, self).__init__(args) def get_parser(): parser = argparse.ArgumentParser( description='Bring changes from the repository into the working copy', prog='vcs pull') parser.add_argument_group('"pull" command parameters') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, PullCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/push.py000066400000000000000000000014211410413400700200160ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class PushCommand(Command): command = 'push' help = 'Push changes from the working copy to the repository' def __init__(self, args): super(PushCommand, self).__init__(args) def get_parser(): parser = argparse.ArgumentParser( description='Push changes from the working copy to the repository', prog='vcs push') parser.add_argument_group('"push" command parameters') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, PushCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/remotes.py000066400000000000000000000013571410413400700205250ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class RemotesCommand(Command): command = 'remotes' help = 'Show the URL of the repository' def __init__(self, args): super(RemotesCommand, self).__init__(args) def get_parser(): parser = argparse.ArgumentParser( description='Show the URL of the repository', prog='vcs remotes') parser.add_argument_group('"remotes" command parameters') return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, RemotesCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/status.py000066400000000000000000000016171410413400700203710ustar00rootroot00000000000000import argparse import sys from vcstool.streams import set_streams from .command import Command from .command import simple_main class StatusCommand(Command): command = 'status' help = 'Show the working tree status' def __init__(self, args): super(StatusCommand, self).__init__(args) self.quiet = args.quiet def get_parser(): parser = argparse.ArgumentParser( description='Show the working tree status', prog='vcs status') group = parser.add_argument_group('"status" command parameters') group.add_argument( '-q', '--quiet', action='store_true', default=False, help="Don't show unversioned items") return parser def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() return simple_main(parser, StatusCommand, args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/validate.py000066400000000000000000000053121410413400700206330ustar00rootroot00000000000000import argparse import sys from vcstool.clients import vcstool_clients from vcstool.commands.import_ import get_repositories from vcstool.executor import ansi from vcstool.executor import execute_jobs from vcstool.executor import output_results from vcstool.streams import set_streams from .command import add_common_arguments from .command import Command class ValidateCommand(Command): command = 'validate' help = 'Validate the repository list file' def __init__(self, args, url, version=None): super(ValidateCommand, self).__init__(args) self.url = url self.version = version self.retry = args.retry def get_parser(): parser = argparse.ArgumentParser( description='Validate a repositories file', prog='vcs validate') group = parser.add_argument_group('"validate" command parameters') group.add_argument( '--input', type=argparse.FileType('r'), default='-') group.add_argument( '--retry', type=int, metavar='N', default=2, help='Retry commands requiring network access N times on failure') return parser def generate_jobs(repos, args): jobs = [] for path, repo in repos.items(): clients = [c for c in vcstool_clients if c.type == repo['type']] if not clients: from vcstool.clients.none import NoneClient job = { 'client': NoneClient(path), 'command': None, 'cwd': path, 'output': "Repository type '%s' is not supported" % repo['type'], 'returncode': NotImplemented } jobs.append(job) continue client = clients[0](path) args.path = None # expected to be present command = ValidateCommand( args, repo['url'], str(repo['version']) if 'version' in repo else None) job = {'client': client, 'command': command} jobs.append(job) return jobs def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) parser = get_parser() add_common_arguments( parser, skip_nested=True, path_nargs=False) args = parser.parse_args(args) try: repos = get_repositories(args.input) except RuntimeError as e: print(ansi('redf') + str(e) + ansi('reset'), file=sys.stderr) return 1 jobs = generate_jobs(repos, args) results = execute_jobs( jobs, show_progress=True, number_of_workers=args.workers, debug_jobs=args.debug) output_results(results, hide_empty=args.hide_empty) any_error = any(r['returncode'] for r in results) return 1 if any_error else 0 if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/commands/vcs.py000066400000000000000000000016201410413400700176330ustar00rootroot00000000000000import sys from vcstool.commands.help import get_entrypoint from vcstool.commands.help import get_parser from vcstool.commands.help import main as help_main from vcstool.streams import set_streams def main(args=None, stdout=None, stderr=None): set_streams(stdout=stdout, stderr=stderr) # no help to extract command first (which might be followed by --help) parser = get_parser(add_help=False) ns, _ = parser.parse_known_args(args) args = args if args is not None else sys.argv[1:] # relay to specific command if ns.command and ns.command != 'help': entrypoint = get_entrypoint(ns.command) if not entrypoint: return 1 args.remove(ns.command) return entrypoint(args) # remove help command if specified if ns.command: args.remove(ns.command) return help_main(args) if __name__ == '__main__': sys.exit(main()) vcstool-0.3.0/vcstool/crawler.py000066400000000000000000000017141410413400700167020ustar00rootroot00000000000000import os from . import vcstool_clients def find_repositories(paths, nested=False): repos = [] visited = [] for path in paths: _find_repositories(path, repos, visited, nested=nested) return repos def _find_repositories(path, repos, visited, nested=False): abs_path = os.path.abspath(path) if abs_path in visited: return visited.append(abs_path) client = get_vcs_client(path) if client: repos.append(client) if not nested: return try: listdir = os.listdir(path) except OSError: listdir = [] for name in sorted(listdir): subpath = os.path.join(path, name) if not os.path.isdir(subpath): continue _find_repositories(subpath, repos, visited, nested=nested) def get_vcs_client(path): for client_class in vcstool_clients: if client_class.is_repository(path): return client_class(path) return None vcstool-0.3.0/vcstool/executor.py000066400000000000000000000220251410413400700170770ustar00rootroot00000000000000import logging import os from queue import Empty, Queue import sys import threading import traceback logger = logging.getLogger(__name__) logging.basicConfig() # Detect special Windows shells that do not support mixes of # backslashes & forward slashes; for those shells, we want to # output POSIX paths, i.e. forward slashes only windows_force_posix = \ sys.platform == 'win32' and '/' in os.environ.get('_', '') def fix_output_path(path): global windows_force_posix return path.replace('\\', '/') if windows_force_posix else path def output_repositories(clients): from vcstool.streams import stdout ordered_clients = {client.path: client for client in clients} for k in sorted(ordered_clients.keys()): client = ordered_clients[k] print( '%s (%s)' % (fix_output_path(k), client.__class__.type), file=stdout) def generate_jobs(clients, command): jobs = [] realpaths = {} for client in clients: # check if client is a duplicate of another path realpath = os.path.realpath(client.path) if realpath not in realpaths: realpaths[realpath] = [client.path] else: # override command on client to ignore multiple invocations # on same repository duplicate_path = realpaths[realpath][0] realpaths[realpath].append(client.path) method_name = command.__class__.command method = getattr(client, method_name, None) if method is not None: setattr(client, method_name, DuplicateCommandHandler( client, duplicate_path)) job = {'client': client, 'command': command} jobs.append(job) return jobs class DuplicateCommandHandler(object): def __init__(self, client, duplicate_path): self.client = client self.duplicate_path = duplicate_path def __call__(self, _command): return { 'cmd': '', 'cwd': self.client.path, 'output': "Same repository as '%s'" % self.duplicate_path, 'returncode': None } def get_ready_job(jobs): for job in jobs: if not job.get('depends', set()): jobs.remove(job) return job return None def execute_jobs( jobs, show_progress=False, number_of_workers=10, debug_jobs=False ): global windows_force_posix from vcstool.streams import stdout if debug_jobs: logger.setLevel(logging.DEBUG) if windows_force_posix: logger.debug('force POSIX paths on Windows') results = [] job_queue = Queue() result_queue = Queue() # create worker threads workers = [] for _ in range(min(number_of_workers, len(jobs))): worker = Worker(job_queue, result_queue) workers.append(worker) # fill job_queue with jobs for each worker pending_jobs = list(jobs) running_job_paths = [] while job_queue.qsize() < len(workers): job = get_ready_job(pending_jobs) if not job: break running_job_paths.append(job['client'].path) logger.debug("started '%s'" % job['client'].path) job_queue.put(job) logger.debug('ongoing %s' % running_job_paths) # start all workers [w.start() for w in workers] # collect results while len(results) < len(jobs): (job, result) = result_queue.get() logger.debug("finished '%s'" % job['client'].path) running_job_paths.remove(result['job']['client'].path) if show_progress and len(jobs) > 1: if result['returncode'] == NotImplemented: stdout.write('s') elif result['returncode']: stdout.write('E') else: stdout.write('.') if debug_jobs: stdout.write('\n') stdout.flush() result.update(job) results.append(result) if pending_jobs: for pending_job in pending_jobs: pending_job.get('depends', set()).discard(job['client'].path) while job_queue.qsize() < len(workers): job = get_ready_job(pending_jobs) if not job: break running_job_paths.append(job['client'].path) logger.debug("started '%s'" % job['client'].path) job_queue.put(job) assert running_job_paths if running_job_paths: logger.debug('ongoing ' + str(running_job_paths)) if show_progress and len(jobs) > 1 and not debug_jobs: print('', file=stdout) # finish progress line # join all workers for w in workers: w.done = True [w.join() for w in workers] return results class Worker(threading.Thread): def __init__(self, job_queue, result_queue): super(Worker, self).__init__() self.daemon = True self.done = False self.job_queue = job_queue self.result_queue = result_queue def run(self): # process all incoming jobs while not self.done: try: # fetch next job job = self.job_queue.get(timeout=0.1) # process job result = self.process_job(job) # send result self.result_queue.put((job, result)) except Empty: pass def process_job(self, job): command = job['command'] if not command: return { 'cmd': '', 'job': job, 'output': job['output'], 'returncode': 1 } method_name = command.__class__.command try: method = getattr(job['client'], method_name, None) if method is None: return { 'cmd': '%s.%s(%s)' % ( job['client'].__class__.type, method_name, job['command'].__class__.command), 'job': job, 'output': "Command '%s' not implemented for client '%s'" % ( job['command'].__class__.command, job['client'].__class__.type), 'returncode': NotImplemented } result = method(job['command']) result['job'] = job return result except Exception as e: exc_tb = sys.exc_info()[2] filename, lineno, _, _ = traceback.extract_tb(exc_tb)[-1] return { 'cmd': '%s.%s(%s)' % ( job['client'].__class__.type, method_name, job['command'].__class__.command), 'job': job, 'output': "Invocation of command '%s' on client '%s' failed: " '%s: %s (%s:%s)' % ( job['command'].__class__.command, job['client'].__class__.type, type(e).__name__, e, filename, lineno), 'returncode': 1 } def output_result(result, hide_empty=False): from vcstool.streams import stdout output = result['output'] if hide_empty and result['returncode'] is None: output = '' if result['returncode'] == NotImplemented: if output: output = ansi('yellowf') + output + ansi('reset') elif result['returncode']: if not output: output = 'Failed with return code %d' % result['returncode'] output = ansi('redf') + output + ansi('reset') elif not result['cmd']: if output: output = ansi('yellowf') + output + ansi('reset') if output or not hide_empty: client = result['client'] print( ansi('bluef') + '=== ' + ansi('boldon') + fix_output_path(client.path) + ansi('boldoff') + ' (' + client.__class__.type + ') ===' + ansi('reset'), file=stdout) if output: try: print(output, file=stdout) except UnicodeEncodeError: print( output.encode(sys.getdefaultencoding(), 'replace'), file=stdout) def output_results(results, output_handler=output_result, hide_empty=False): # output results in alphabetic order path_to_idx = { result['client'].path: i for i, result in enumerate(results)} idxs_in_order = [path_to_idx[path] for path in sorted(path_to_idx.keys())] for i in idxs_in_order: output_handler(results[i], hide_empty=hide_empty) USE_COLOR = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() # disable color on Windows except if ConEmuANSI is explicitly enabled if os.name == 'nt' and os.environ.get('ConEmuANSI', None) != 'ON': USE_COLOR = False def ansi(keyword): if not USE_COLOR: return '' codes = { 'bluef': '\033[34m', 'boldon': '\033[1m', 'boldoff': '\033[22m', 'cyanf': '\033[36m', 'redf': '\033[31m', 'reset': '\033[0m', 'yellowf': '\033[33m', } if keyword in codes: return codes[keyword] return '' vcstool-0.3.0/vcstool/streams.py000066400000000000000000000005201410413400700167130ustar00rootroot00000000000000import sys stdout = sys.stdout stderr = sys.stderr def set_streams(stdout=None, stderr=None): _set_streams(stdout_=stdout, stderr_=stderr) def _set_streams(stdout_=None, stderr_=None): global stdout global stderr if stdout_ is not None: stdout = stdout_ if stderr_ is not None: stderr = stderr_ vcstool-0.3.0/vcstool/util.py000066400000000000000000000007041410413400700162160ustar00rootroot00000000000000from errno import EACCES, EPERM import os from shutil import rmtree as shutil_rmtree import stat import sys def rmtree(path): kwargs = {} if sys.platform == 'win32': kwargs['onerror'] = _onerror_windows return shutil_rmtree(path, **kwargs) def _onerror_windows(function, path, excinfo): if isinstance(excinfo[1], OSError) and excinfo[1].errno in (EACCES, EPERM): os.chmod(path, stat.S_IWRITE) function(path)