pax_global_header 0000666 0000000 0000000 00000000064 14104134007 0014504 g ustar 00root root 0000000 0000000 52 comment=c20d4b48df4e5c36976cab19d00577a19649027c
vcstool-0.3.0/ 0000775 0000000 0000000 00000000000 14104134007 0013175 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/.github/ 0000775 0000000 0000000 00000000000 14104134007 0014535 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14104134007 0016572 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000002304 14104134007 0017707 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000053 14104134007 0015163 0 ustar 00root root 0000000 0000000 *.pyc
build
deb_dist
dist
vcstool.egg-info
vcstool-0.3.0/CONTRIBUTING.md 0000664 0000000 0000000 00000001171 14104134007 0015426 0 ustar 00root root 0000000 0000000 Any 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/LICENSE 0000664 0000000 0000000 00000026123 14104134007 0014206 0 ustar 00root root 0000000 0000000
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.in 0000664 0000000 0000000 00000000247 14104134007 0014736 0 ustar 00root root 0000000 0000000 include 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/Makefile 0000664 0000000 0000000 00000000600 14104134007 0014631 0 ustar 00root root 0000000 0000000 .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.rst 0000664 0000000 0000000 00000022165 14104134007 0014672 0 ustar 00root root 0000000 0000000 What 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.yaml 0000664 0000000 0000000 00000000522 14104134007 0017045 0 ustar 00root root 0000000 0000000 artifacts:
- 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/ 0000775 0000000 0000000 00000000000 14104134007 0014664 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/scripts/vcs 0000775 0000000 0000000 00000000141 14104134007 0015401 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.vcs import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-branch 0000775 0000000 0000000 00000000144 14104134007 0016637 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.branch import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-bzr 0000775 0000000 0000000 00000000154 14104134007 0016200 0 ustar 00root root 0000000 0000000 #!/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-custom 0000775 0000000 0000000 00000000144 14104134007 0016714 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.custom import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-diff 0000775 0000000 0000000 00000000142 14104134007 0016310 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.diff import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-export 0000775 0000000 0000000 00000000144 14104134007 0016723 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.export import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-git 0000775 0000000 0000000 00000000154 14104134007 0016166 0 ustar 00root root 0000000 0000000 #!/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-help 0000775 0000000 0000000 00000000142 14104134007 0016330 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.help import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-hg 0000775 0000000 0000000 00000000152 14104134007 0015777 0 ustar 00root root 0000000 0000000 #!/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-import 0000775 0000000 0000000 00000000145 14104134007 0016715 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.import_ import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-log 0000775 0000000 0000000 00000000141 14104134007 0016160 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.log import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-pull 0000775 0000000 0000000 00000000142 14104134007 0016354 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.pull import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-push 0000775 0000000 0000000 00000000142 14104134007 0016357 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.push import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-remotes 0000775 0000000 0000000 00000000145 14104134007 0017061 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.remotes import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-status 0000775 0000000 0000000 00000000144 14104134007 0016725 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.status import main
sys.exit(main() or 0)
vcstool-0.3.0/scripts/vcs-svn 0000775 0000000 0000000 00000000154 14104134007 0016211 0 ustar 00root root 0000000 0000000 #!/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-validate 0000775 0000000 0000000 00000000146 14104134007 0017175 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import sys
from vcstool.commands.validate import main
sys.exit(main() or 0)
vcstool-0.3.0/setup.py 0000664 0000000 0000000 00000004467 14104134007 0014722 0 ustar 00root root 0000000 0000000 from 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.sh 0000664 0000000 0000000 00000000563 14104134007 0014675 0 ustar 00root root 0000000 0000000 if [ "$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.cfg 0000664 0000000 0000000 00000000166 14104134007 0014762 0 ustar 00root root 0000000 0000000 [DEFAULT]
No-Python2:
Depends3: python3-setuptools, python3-yaml
Conflicts3: python-vcstool
X-Python3-Version: >= 3.5
vcstool-0.3.0/test/ 0000775 0000000 0000000 00000000000 14104134007 0014154 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/test/branch.txt 0000664 0000000 0000000 00000000275 14104134007 0016156 0 ustar 00root root 0000000 0000000 ....
=== ./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.txt 0000664 0000000 0000000 00000000102 14104134007 0016347 0 ustar 00root root 0000000 0000000 The available VCS clients are:
bzr
git
hg
svn
tar
zip
vcstool-0.3.0/test/commands.txt 0000664 0000000 0000000 00000000107 14104134007 0016514 0 ustar 00root root 0000000 0000000 branch custom diff export import log pull push remotes status validate
vcstool-0.3.0/test/custom_describe.txt 0000664 0000000 0000000 00000000072 14104134007 0020066 0 ustar 00root root 0000000 0000000 ..
=== ./hash (git) ===
0.1.26
=== ./tag (git) ===
0.1.27
vcstool-0.3.0/test/diff_hide.txt 0000664 0000000 0000000 00000000556 14104134007 0016624 0 ustar 00root root 0000000 0000000 ....
=== ./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.txt 0000664 0000000 0000000 00000000415 14104134007 0017422 0 ustar 00root root 0000000 0000000 repositories:
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.txt 0000664 0000000 0000000 00000000353 14104134007 0021474 0 ustar 00root root 0000000 0000000 repositories:
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.txt 0000664 0000000 0000000 00000003330 14104134007 0016226 0 ustar 00root root 0000000 0000000 ......
=== ./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.txt 0000664 0000000 0000000 00000003765 14104134007 0017773 0 ustar 00root root 0000000 0000000 ......
=== ./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.repos 0000664 0000000 0000000 00000001521 14104134007 0016200 0 ustar 00root root 0000000 0000000 repositories:
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.repos 0000664 0000000 0000000 00000000620 14104134007 0016261 0 ustar 00root root 0000000 0000000 repositories:
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.txt 0000664 0000000 0000000 00000001177 14104134007 0016702 0 ustar 00root root 0000000 0000000 ..
=== ./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.txt 0000664 0000000 0000000 00000001135 14104134007 0020101 0 ustar 00root root 0000000 0000000 === . (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.txt 0000664 0000000 0000000 00000000712 14104134007 0015671 0 ustar 00root root 0000000 0000000 ....
=== ./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.txt 0000664 0000000 0000000 00000001261 14104134007 0016556 0 ustar 00root root 0000000 0000000 ......
=== ./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.txt 0000664 0000000 0000000 00000001170 14104134007 0017733 0 ustar 00root root 0000000 0000000 ......
=== ./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.txt 0000664 0000000 0000000 00000000721 14104134007 0017604 0 ustar 00root root 0000000 0000000 ......
=== ./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.txt 0000664 0000000 0000000 00000001233 14104134007 0017602 0 ustar 00root root 0000000 0000000 ./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.txt 0000664 0000000 0000000 00000000707 14104134007 0016244 0 ustar 00root root 0000000 0000000 ....
=== ./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.py 0000664 0000000 0000000 00000042157 14104134007 0017377 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000004512 14104134007 0016741 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000002023 14104134007 0017255 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001514 14104134007 0016507 0 ustar 00root root 0000000 0000000 ......
=== 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.txt 0000664 0000000 0000000 00000000677 14104134007 0016602 0 ustar 00root root 0000000 0000000 ....
=== 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.txt 0000664 0000000 0000000 00000000330 14104134007 0017473 0 ustar 00root root 0000000 0000000 ......
=== 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/ 0000775 0000000 0000000 00000000000 14104134007 0017035 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/vcstool-completion/vcs.bash 0000664 0000000 0000000 00000000342 14104134007 0020466 0 ustar 00root root 0000000 0000000 function _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.fish 0000664 0000000 0000000 00000000737 14104134007 0020512 0 ustar 00root root 0000000 0000000 complete -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.tcsh 0000664 0000000 0000000 00000000056 14104134007 0020514 0 ustar 00root root 0000000 0000000 complete vcs 'p/1/`vcs --commands`/' 'C/*/d/'
vcstool-0.3.0/vcstool-completion/vcs.zsh 0000664 0000000 0000000 00000000253 14104134007 0020356 0 ustar 00root root 0000000 0000000 function _vcs()
{
local opts
reply=()
if [[ ${CURRENT} == 2 ]]; then
opts=`vcs --commands`
reply=(${=opts})
fi
}
compctl -K "_vcs" "vcs"
vcstool-0.3.0/vcstool/ 0000775 0000000 0000000 00000000000 14104134007 0014666 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/vcstool/__init__.py 0000664 0000000 0000000 00000000104 14104134007 0016772 0 ustar 00root root 0000000 0000000 from .clients import vcstool_clients # noqa
__version__ = '0.3.0'
vcstool-0.3.0/vcstool/clients/ 0000775 0000000 0000000 00000000000 14104134007 0016327 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/vcstool/clients/__init__.py 0000664 0000000 0000000 00000001540 14104134007 0020440 0 ustar 00root root 0000000 0000000 vcstool_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.py 0000664 0000000 0000000 00000016001 14104134007 0017474 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000075343 14104134007 0017500 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000027302 14104134007 0017303 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000000250 14104134007 0017635 0 ustar 00root root 0000000 0000000 from .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.py 0000664 0000000 0000000 00000020517 14104134007 0017514 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000006653 14104134007 0017501 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000007537 14104134007 0020502 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000010444 14104134007 0017506 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14104134007 0016467 5 ustar 00root root 0000000 0000000 vcstool-0.3.0/vcstool/commands/__init__.py 0000664 0000000 0000000 00000002000 14104134007 0020570 0 ustar 00root root 0000000 0000000 from .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.py 0000664 0000000 0000000 00000001540 14104134007 0020276 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000006727 14104134007 0020473 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000006725 14104134007 0020365 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001612 14104134007 0017751 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000010027 14104134007 0020362 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000010210 14104134007 0017763 0 ustar 00root root 0000000 0000000 import 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_.py 0000664 0000000 0000000 00000021137 14104134007 0020516 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000003162 14104134007 0017624 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001427 14104134007 0020021 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001421 14104134007 0020016 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001357 14104134007 0020525 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001617 14104134007 0020371 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000005312 14104134007 0020633 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001620 14104134007 0017633 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001714 14104134007 0016702 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000022025 14104134007 0017077 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000000520 14104134007 0016713 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000000704 14104134007 0016216 0 ustar 00root root 0000000 0000000 from 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)