[ros-vcstools] 01/31: Imported Upstream version 0.1.32
Jochen Sprickerhof
jspricke-guest at moszumanska.debian.org
Sun Oct 18 14:23:20 UTC 2015
This is an automated email from the git hooks/post-receive script.
jspricke-guest pushed a commit to branch master
in repository ros-vcstools.
commit 9b562f410543123c71490e481e801357cb4511a2
Author: Leopold Palomo-Avellaneda <leopold.palomo at upc.edu>
Date: Fri Nov 28 08:59:36 2014 +0100
Imported Upstream version 0.1.32
---
.gitignore | 3 +
.hgignore | 14 +
.travis.yml | 65 ++++
LICENSE | 32 ++
Makefile | 50 +++
README.rst | 38 ++
doc/Makefile | 135 +++++++
doc/changelog.rst | 206 +++++++++++
doc/conf.py | 219 ++++++++++++
doc/developers_guide.rst | 80 +++++
doc/index.rst | 72 ++++
doc/modules.rst | 7 +
doc/vcsclient.rst | 101 ++++++
doc/vcstools.rst | 69 ++++
dput.cf | 24 ++
rosdoc.yaml | 2 +
setup.py | 38 ++
setup.sh | 10 +
src/vcstools/__init__.py | 76 ++++
src/vcstools/__version__.py | 1 +
src/vcstools/bzr.py | 287 +++++++++++++++
src/vcstools/common.py | 314 +++++++++++++++++
src/vcstools/git.py | 759 ++++++++++++++++++++++++++++++++++++++++
src/vcstools/hg.py | 328 +++++++++++++++++
src/vcstools/svn.py | 280 +++++++++++++++
src/vcstools/tar.py | 181 ++++++++++
src/vcstools/vcs_abstraction.py | 138 ++++++++
src/vcstools/vcs_base.py | 244 +++++++++++++
stdeb.cfg | 4 +
test/__init__.py | 0
test/test_base.py | 160 +++++++++
test/test_bzr.py | 352 +++++++++++++++++++
test/test_code_format.py | 29 ++
test/test_git.py | 729 ++++++++++++++++++++++++++++++++++++++
test/test_git_subm.py | 233 ++++++++++++
test/test_hg.py | 318 +++++++++++++++++
test/test_svn.py | 346 ++++++++++++++++++
test/test_tar.py | 206 +++++++++++
test/test_vcs_abstraction.py | 50 +++
39 files changed, 6200 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8688240
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.pyc
+.DS_Store
+.coverage
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..34c80ae
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,14 @@
+syntax: glob
+*.orig
+*.swp
+*.pyc
+*.DS_Store
+*~
+*.log
+.coverage
+doc-pak/*
+src/vcstools.egg-info/*
+nosetests.xml
+syntax: regexp
+(target|build|dist)/.*
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e57c3ea
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,65 @@
+language: python
+python:
+ - "2.6"
+ - "2.7"
+ - "3.2"
+ - "3.3"
+env:
+ # lucid
+ - HG='1.4.2' BZR=2.1.4 GIT=v1.7.0.4 SVN=1.6.6 PYYAML=3.10
+ # natty
+ # - HG='1.6.3' BZR=2.3.4 GIT=v1.7.4.1 SVN=1.6.6 PYYAML=3.10
+ # - HG='1.7.5' BZR=2.3.4 GIT=v1.7.4.1 SVN=1.6.6 PYYAML=3.10
+ # # oneiric
+ # - HG='1.9.1' BZR=2.4.1 GIT=v1.7.5.4 SVN=1.6.12 PYYAML=3.10
+ # precise
+ - HG='2.0.2' BZR=2.5.0 GIT=v1.7.9.5 SVN=1.6.17 PYYAML=3.10
+ # quantal
+ - HG='2.2.2' BZR=2.6.0~beta2 GIT=v1.7.10.4 SVN=1.7.5 PYYAML=3.10
+# bzr 2.1.1 only builds with python 2.6
+matrix:
+ exclude:
+ - python: "2.7"
+ env: HG='1.4.2' BZR=2.1.4 GIT=v1.7.0.4 SVN=1.6.6 PYYAML=3.10
+ - python: "3.2"
+ env: HG='1.4.2' BZR=2.1.4 GIT=v1.7.0.4 SVN=1.6.6 PYYAML=3.10
+ - python: "3.3"
+ env: HG='1.4.2' BZR=2.1.4 GIT=v1.7.0.4 SVN=1.6.6 PYYAML=3.10
+before_install:
+ - export REPO=`pwd`
+install:
+ - sudo apt-get install -qq python3-yaml python3-dev
+ - sudo apt-get install -qq libapr1 libapr1-dev libaprutil1 libaprutil1-dev libneon27 libneon27-dev libc6-dev g++ gcc
+ - echo $PYTHONPATH
+ - python -c 'import sys;print(sys.path)'
+ - python setup.py build
+# cannot build mercurial and bzr using py3k, but cannot use python2 in python2.6 case
+ - export PY2K=`python -c 'import sys; print("python2" if (sys.version_info[0] == 3) else "python")'`
+ - pip install coverage
+ - pip install cython
+ - pip install pep8
+ - pip install "pyyaml<=$PYYAML" > pyaml-warnings.log 2>&1 || (cat pyaml-warnings.log && false)
+ - pip install python-dateutil
+ - cd $HOME/builds && wget http://mercurial.selenic.com/release/mercurial-$HG.tar.gz && tar -xf mercurial-$HG.tar.gz && cd mercurial-$HG && sudo $PY2K setup.py install > hg_install.log 2>&1 || (cat hg_install.log && false)
+# did not find single source for old git tarballs
+ - cd $HOME/builds && git clone git://git.kernel.org/pub/scm/git/git.git && cd git && git checkout $GIT && make prefix=/usr all > git_install.log 2>&1 || (cat git_install.log && false) && sudo make prefix=/usr install && cd $HOME/builds
+ - cd $HOME/builds && wget http://archive.ubuntu.com/ubuntu/pool/main/b/bzr/bzr_$BZR.orig.tar.gz && tar -xf bzr_$BZR.orig.tar.gz && cd bzr-* && sudo $PY2K setup.py install build_ext --allow-python-fallback > bzr_install.log 2>&1 || (cat bzr_install.log && false) && cd $HOME/builds
+# subversion has complex dependencies
+ - cd $HOME/builds && wget http://archive.apache.org/dist/subversion/subversion-$SVN.tar.gz && tar -xf subversion-$SVN.tar.gz && cd subversion-$SVN && ./configure --without-berkeley-db --without-apache --without-neon --without-swig --disable-nls > svn_install.log 2>&1 || (cat svn_install.log && false) && make -j && sudo make install > svn_install.log 2>&1 || (cat svn_install.log && false) && cd $HOME/builds
+ - hg --version
+ - bzr --version
+ - git --version
+ - svn --version
+# Set git config to silence some stuff in the tests
+ - git config --global user.email "foo at example.com"
+ - git config --global user.name "Foo Bar"
+# Set the hg user
+ - echo -e "[ui]\nusername = Your Name <your at mail.com>" >> ~/.hgrc
+# Set the bzr user
+ - bzr whoami "Your Name <name at example.com>"
+ - cd $REPO
+# command to run tests
+script:
+ - nosetests --with-coverage --cover-package vcstools
+notifications:
+ email: false
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c34c437
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,32 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..794b582
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,50 @@
+.PHONY: all setup clean_dist distro clean install deb_dist upload-packages upload-building upload testsetup test
+
+NAME=vcstools
+VERSION=$(shell grep version ./src/vcstools/__version__.py | sed 's,version = ,,')
+
+OUTPUT_DIR=deb_dist
+
+
+all:
+ echo "noop for debbuild"
+
+setup:
+ echo "building version ${VERSION}"
+
+clean_dist:
+ -rm -f MANIFEST
+ -rm -rf dist
+ -rm -rf deb_dist
+
+distro: clean_dist setup
+ python setup.py sdist
+
+push: distro
+ python setup.py sdist register upload
+ scp dist/${NAME}-${VERSION}.tar.gz ros at ftp-osl.osuosl.org:/home/ros/data/download.ros.org/downloads/${NAME}
+
+clean: clean_dist
+ echo "clean"
+
+install: distro
+ sudo checkinstall python setup.py install
+
+deb_dist:
+ # need to convert unstable to each distro and repeat
+ python setup.py --command-packages=stdeb.command sdist_dsc --workaround-548392=False bdist_deb
+
+upload-packages: deb_dist
+ dput -u -c dput.cf all-shadow-fixed ${OUTPUT_DIR}/${NAME}_${VERSION}-1_amd64.changes
+ dput -u -c dput.cf all-ros ${OUTPUT_DIR}/${NAME}_${VERSION}-1_amd64.changes
+
+upload-building: deb_dist
+ dput -u -c dput.cf all-building ${OUTPUT_DIR}/${NAME}_${VERSION}-1_amd64.changes
+
+upload: upload-building upload-packages
+
+testsetup:
+ echo "running tests"
+
+test: testsetup
+ nosetests --with-coverage --cover-package=vcstools --with-xunit
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..05ed6e5
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,38 @@
+vcstools
+========
+
+The vcstools module provides a Python API for interacting with different version control systems (VCS/SCMs).
+
+See http://www.ros.org/doc/independent/api/vcstools/html/
+
+Installing
+----------
+
+Install the latest release on Ubuntu using apt-get::
+
+ $ sudo apt-get install vcstools
+
+On other Systems, use the pypi package::
+
+ $ pip install vcstools
+
+Developer Environment
+---------------------
+
+source setup.sh to include the src folder in your PYTHONPATH.
+
+Testing
+-------
+
+Use the python library nose to test::
+
+ $ nosetests
+
+To test with coverage, make sure to have python-coverage installed and run::
+
+ $ nosetests --with-coverage --cover-package vcstools
+
+To run python3 compatibility tests, run either::
+
+ $ nosetests3
+ $ python3 -m unittest discover --pattern*.py
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..e8bacd1
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,135 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/vcstools.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vcstools.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/vcstools"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/vcstools"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+upload: html
+ # set write permission for group so that everybody can overwrite existing files on the webserver
+ chmod -R g+w _build/html/
+ scp -pr _build/html/ rosbot at ros.osuosl.org:/home/rosbot/docs/independent/api/vcstools
diff --git a/doc/changelog.rst b/doc/changelog.rst
new file mode 100644
index 0000000..13e2f15
--- /dev/null
+++ b/doc/changelog.rst
@@ -0,0 +1,206 @@
+Changelog
+=========
+
+0.1
+===
+
+0.1.31
+------
+
+- Fix submodule support on checkout #71
+
+0.1.30
+------
+
+- use netrc to download tars from private repos, also will work for private rosinstall files
+- Fix checks for empty repository #62
+
+0.1.29
+------
+
+- fix #57 shallow checkout of non-master breaks with git >= 1.8.0
+- unit test fixes
+
+0.1.28
+------
+
+- test of new upload method
+
+0.1.27
+------
+
+- fix #51 hg status and diff dont work if workspace is inside hg repo
+- fix #47 several performance improvements by removing unecessary update actions after checkout
+- fix #46 https tar download fails behind proxy
+- fix #45 sometimes commands run forever
+- fix #44 minor bug when checking out from repo with default branch not master
+- fix #41 improvedAPI, get_vcs_client function part of vcstools module
+
+0.1.26
+------
+
+- fix #38 git commands fail in local repositories with many (>2000) references
+- fix #31 get_log() svn xml not available on Ubuntu Lucid (hg 1.4.2)
+- fix #37 update() returns True even when fetch failed
+
+0.1.25
+------
+
+- minor bugfixes
+- travis-ci config file
+- fix unit tests for svn diff&status ordering changes
+- deprecated VcsClient Class
+- added get_log function
+
+0.1.24
+------
+
+- fix git update return value to False when fast-forward not possible due to diverge
+- fix. svn certificate prompt invisible, svn checkout and update become verbose due to this
+
+0.1.22
+------
+
+- Changed the way that git implements detect_presence to fix a bug with submodules in newer versions of git
+- fix for git single quotes on Windows
+- minor internal api bug where a git function always returned True
+- fix gub in svn export_repository
+
+0.1.21
+------
+
+- bugfix #66: hg http username prompt hidden
+- add export_repository method to vcs_base and all implementations with tests
+- bugfix #64: unicode decoding problems
+
+0.1.20
+------
+
+- rosws update --verbose for git prints small message when rebasing
+- improved python3 compatibility
+
+0.1.19
+------
+- more python3 compatibility
+- code style improved
+- match_url to compare bzr shortcuts to real urls
+- more unit tests
+- get_status required to end with newline, to fix #55
+
+0.1.18
+------
+- added shallow flag to API, implemented for git
+
+0.1.17
+------
+
+- svn stdout output on get_version removed
+
+0.1.16
+------
+
+- All SCMs show some output when update caused changes
+- All SCMs have verbose option to show all changes done on update
+- bugfix for bazaar getUrl() being a joined abspath
+- bugfix for not all output being shown when requested
+
+
+0.1.15
+------
+
+- Added pyyaml as a proper dependency, removed detection code.
+- remove use of tar entirely, switch to tarfile module
+- fix #36 allowing for tar being bsdtar on OSX
+
+0.1.14
+------
+
+- Added tarball uncompression.
+
+0.1.13
+------
+
+- added this changelog
+- git get-version fetches only when local lookup fails
+- hg get-version pulls if label not found
+- Popen error message incudes cwd path
+
+0.1.12
+------
+
+- py_checker clean after all refactorings since 0.1.0
+
+0.1.11
+------
+
+- svn and hg update without user interaction
+- bugfix #30
+- minor bugfixes
+
+0.1.10
+------
+
+- minor bugs
+
+0.1.9
+-----
+
+- safer sanitization of shell params
+- git diff and stat recurse for submodules
+- base class manages all calls to Popen
+
+0.1.8
+-----
+
+- several bugfixes
+- reverted using shell commands instead of bazaar API
+
+
+0.1.7
+-----
+
+- reverted using shell commands instaed of pysvn and mercurial APIs
+- protection against shell incection attempts
+
+0.1.6
+-----
+
+- bugfixes to svn and bzr
+- unified all calls through Popen
+
+0.1.5
+-----
+
+- missing dependency to dateutil added
+
+0.1.4
+-----
+
+switched shell calls to calls to python API of mercurial, bazaar, py-svn
+
+0.1.3
+-----
+
+- fix #6
+
+0.1.2
+-----
+
+- fix #15
+
+0.1.1
+-----
+
+- more unit tests
+- diverse bugfixes
+- major change to git client behavior, based around git https://kforge.ros.org/vcstools/trac/ticket/1
+
+0.1.0
+-----
+
+- documentation fixes
+
+0.0.3
+-----
+
+- import from svn
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..3c4f45d
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+#
+# vcstools documentation build configuration file, created by
+# sphinx-quickstart on Thu Aug 4 20:58:04 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+sys.path.insert(0, os.path.abspath('../src'))
+
+from vcstools.__version__ import version
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.autosummary']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'vcstools'
+copyright = u'2010, Willow Garage'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = version
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'haiku'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'vcstoolsdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'vcstools.tex', u'vcstools Documentation',
+ u'Tully Foote, Thibault Kruse, Ken Conley', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'vcstools', u'vcstools Documentation',
+ [u'Tully Foote, Thibault Kruse, Ken Conley'], 1)
+]
diff --git a/doc/developers_guide.rst b/doc/developers_guide.rst
new file mode 100644
index 0000000..cddca68
--- /dev/null
+++ b/doc/developers_guide.rst
@@ -0,0 +1,80 @@
+Developer's Guide
+=================
+
+Code API
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ modules
+
+Changelog
+---------
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog
+
+Bug reports and feature requests
+--------------------------------
+
+- `Submit a bug report <https://kforge.ros.org/vcstools/trac/newticket?component=vcstools&type=defect>`_
+- `Submit a feature request <https://kforge.ros.org/vcstools/trac/newticket?component=vcstools&type=enhancement&vcstools>`_
+
+Developer Setup
+---------------
+
+vcstools uses `setuptools <http://pypi.python.org/pypi/setuptools>`_,
+which you will need to download and install in order to run the
+packaging. We use setuptools instead of distutils in order to be able
+use ``setup()`` keys like ``install_requires``.
+
+Configure your :envvar:`PYTHONPATH`::
+
+ cd vcstools
+ . setup.sh
+
+OR::
+
+ cd vcstools
+ python setup.py install
+
+The first will prepend ``vcstools/src`` to your :envvar:`PYTHONPATH`. The second will install vcstools into your dist/site-packages.
+
+Testing
+-------
+
+Install test dependencies
+
+::
+
+ pip install nose
+ pip install mock
+
+
+vcstools uses `Python nose
+<http://readthedocs.org/docs/nose/en/latest/>`_ for testing, which is
+a fairly simple and straightfoward test framework. The vcstools
+mainly use :mod:`unittest` to construct test fixtures, but with nose
+you can also just write a function that starts with the name ``test``
+and use normal ``assert`` statements.
+
+vcstools also uses `mock <http://www.voidspace.org.uk/python/mock/>`_
+to create mocks for testing.
+
+You can run the tests, including coverage, as follows:
+
+::
+
+ cd vcstools
+ make test
+
+
+Documentation
+-------------
+
+Sphinx is used to provide API documentation for vcstools. The documents
+are stored in the ``doc`` subdirectory.
+
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..107afcc
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,72 @@
+vcstools documentation
+======================
+
+.. module:: vcstools
+.. moduleauthor:: Tully Foote <tfoote at willowgarage.com>, Thibault Kruse <kruset at in.tum.de>, Ken Conley <kwc at willowgarage.com>
+
+The :mod:`vcstools` module provides a Python API for interacting with
+different version control systems (VCS/SCMs). The :class:`VcsClient`
+class provides an API for seamless interacting with Git, Mercurial
+(Hg), Bzr and SVN. The focus of the API is manipulating on-disk
+checkouts of source-controlled trees. Its main use is to support the
+`rosinstall` tool.
+
+.. toctree::
+ :maxdepth: 2
+
+ vcsclient
+
+Example::
+
+ import vcstools
+
+ # interrogate an existing tree
+ client = vcstools.VcsClient('svn', '/path/to/checkout')
+
+ print client.get_url()
+ print client.get_version()
+ print client.get_diff()
+
+ # create a new tree
+ client = vcstools.VcsClient('hg', '/path/to/new/checkout')
+ client.checkout('https://bitbucket.org/foo/bar')
+
+
+Installation
+============
+
+vcstools is available on pypi and can be installed via ``pip``
+::
+
+ pip install vcstools
+
+or ``easy_install``:
+
+::
+
+ easy_install vcstools
+
+Using vcstools
+==============
+
+The :mod:`vcstools` module is meant to be used as a normal Python
+module. After it has been installed, you can ``import`` it normally
+and do not need to declare as a ROS package dependency.
+
+
+Advanced: vcstools developers/contributors
+========================================
+
+.. toctree::
+ :maxdepth: 2
+
+ developers_guide
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/doc/modules.rst b/doc/modules.rst
new file mode 100644
index 0000000..cc83a71
--- /dev/null
+++ b/doc/modules.rst
@@ -0,0 +1,7 @@
+Packages
+========
+
+.. toctree::
+ :maxdepth: 4
+
+ vcstools
diff --git a/doc/vcsclient.rst b/doc/vcsclient.rst
new file mode 100644
index 0000000..e673c72
--- /dev/null
+++ b/doc/vcsclient.rst
@@ -0,0 +1,101 @@
+General VCS/SCM API
+===================
+
+.. currentmodule:: vcstools
+
+The :class:`VcsClient` class provides a generic API for
+
+- Subversion (``svn``)
+- Mercurial (``hg``)
+- Git (``git``)
+- Bazaar (``bzr``)
+
+
+
+.. class:: VcsClient(vcs_type, path)
+
+ API for interacting with source-controlled paths independent of
+ actual version-control implementation.
+
+ :param vcs_type: type of VCS to use (e.g. 'svn', 'hg', 'bzr', 'git'), ``str``
+ :param path: filesystem path where code is/will be checked out , ``str``
+
+ .. method:: path_exists() -> bool
+
+ :returns: True if path exists on disk.
+
+ .. method:: get_path() -> str
+
+ :returns: filesystem path this client is initialized with.
+
+ .. method:: get_version([spec=None])
+
+ :param spec: token for identifying repository revision
+ desired. Token might be a tagname, branchname, version-id,
+ or SHA-ID depending on the VCS implementation.
+
+ - svn: anything accepted by ``svn info --help``,
+ e.g. a ``revnumber``, ``{date}``, ``HEAD``, ``BASE``, ``PREV``, or
+ ``COMMITTED``
+ - git: anything accepted by ``git log``, e.g. a tagname,
+ branchname, or sha-id.
+ - hg: anything accepted by ``hg log -r``, e.g. a tagname, sha-ID,
+ revision-number
+ - bzr: revisionspec as returned by ``bzr help revisionspec``,
+ e.g. a tagname or ``revno:<number>``
+
+ :returns: current revision number of the repository. Or if
+ spec is provided, the globally unique identifier
+ (e.g. revision number, or SHA-ID) of a revision specified by
+ some token.
+
+
+ .. method:: checkout(url, [version=''], [verbose=False], [shallow=False])
+
+ Checkout the given URL to the path associated with this client.
+
+ :param url: URL of source control to check out
+ :param version: specific version to check out
+ :param verbose: flag to run verbosely
+ :param shallow: flag to create shallow clone without history
+
+ .. method:: update(version)
+
+ Update the local checkout from upstream source control.
+
+ .. method:: detect_presence() -> bool
+
+ :returns: True if path has a checkout with matching VCS type,
+ e.g. if the type of this client is 'svn', the checkout at
+ the path is managed by Subversion.
+
+ .. method:: get_vcs_type_name() -> str
+
+ :returns: type of VCS this client is initialized with.
+
+ .. method:: get_url() -> str
+
+ :returns: Upstream URL that this code was checked out from.
+
+ .. method:: get_branch_parent()
+
+ (Git Only)
+
+ :returns: parent branch.
+
+ .. method:: get_diff([basepath=None])
+
+ :param basepath: compute diff relative to this path, if provided
+ :returns: A string showing local differences
+
+ .. method:: get_status([basepath=None, [untracked=False]])
+
+ Calls scm status command. semantics of untracked are difficult
+ to generalize. In SVN, this would be new files only. In git,
+ hg, bzr, this would be changes that have not been added for
+ commit.
+
+ :param basepath: status path will be relative to this, if provided.
+ :param untracked: If True, also show changes that would not commit
+ :returns: A string summarizing locally modified files
+
diff --git a/doc/vcstools.rst b/doc/vcstools.rst
new file mode 100644
index 0000000..eacfe05
--- /dev/null
+++ b/doc/vcstools.rst
@@ -0,0 +1,69 @@
+vcstools Package
+================
+
+:mod:`vcstools` Package
+-----------------------
+
+.. automodule:: vcstools
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`bzr` Module
+-----------------
+
+.. automodule:: vcstools.bzr
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`git` Module
+-----------------
+
+.. automodule:: vcstools.git
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`hg` Module
+----------------
+
+.. automodule:: vcstools.hg
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`svn` Module
+-----------------
+
+.. automodule:: vcstools.svn
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`tar` Module
+-----------------
+
+.. automodule:: vcstools.tar
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`vcs_abstraction` Module
+-----------------------------
+
+.. automodule:: vcstools.vcs_abstraction
+ :members:
+ :special-members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`vcs_base` Module
+----------------------
+
+.. automodule:: vcstools.vcs_base
+ :members:
+ :special-members:
+ :undoc-members:
+ :show-inheritance:
+
diff --git a/dput.cf b/dput.cf
new file mode 100644
index 0000000..ce5e6c7
--- /dev/null
+++ b/dput.cf
@@ -0,0 +1,24 @@
+[all-building]
+method = scp
+login = rosbuild
+fqdn = repos.ros.org
+incoming = /var/www/repos/building/queue/all
+run_dinstall = 0
+post_upload_command = ssh rosbuild at repos.ros.org -- /usr/bin/reprepro -b /var/www/repos/building --ignore=emptyfilenamepart -V processincoming all
+
+
+[all-shadow-fixed]
+method = scp
+login = rosbuild
+fqdn = repos.ros.org
+incoming = /var/www/repos/ros-shadow-fixed/ubuntu/queue/all
+run_dinstall = 0
+post_upload_command = ssh rosbuild at repos.ros.org -- /usr/bin/reprepro -b /var/www/repos/ros-shadow-fixed/ubuntu --ignore=emptyfilenamepart -V processincoming all
+
+[all-ros]
+method = scp
+login = rosbuild
+fqdn = repos.ros.org
+incoming = /var/www/repos/ros/ubuntu/queue/all
+run_dinstall = 0
+post_upload_command = ssh rosbuild at repos.ros.org -- /usr/bin/reprepro -b /var/www/repos/ros/ubuntu --ignore=emptyfilenamepart -V processincoming all
diff --git a/rosdoc.yaml b/rosdoc.yaml
new file mode 100644
index 0000000..d21d73a
--- /dev/null
+++ b/rosdoc.yaml
@@ -0,0 +1,2 @@
+ - builder: sphinx
+ sphinx_root_dir: doc
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ba8d642
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+from setuptools import setup
+
+import imp
+
+
+def get_version():
+ ver_file = None
+ try:
+ ver_file, pathname, description = imp.find_module('__version__', ['src/vcstools'])
+ vermod = imp.load_module('__version__', ver_file, pathname, description)
+ version = vermod.version
+ return version
+ finally:
+ if ver_file is not None:
+ ver_file.close()
+
+
+setup(name='vcstools',
+ version=get_version(),
+ packages=['vcstools'],
+ package_dir={'': 'src'},
+ scripts=[],
+ install_requires=['pyyaml', 'python-dateutil'],
+ author="Tully Foote, Thibault Kruse, Ken Conley",
+ author_email="tfoote at osrfoundation.org",
+ url="http://wiki.ros.org/vcstools",
+ download_url="http://download.ros.org/downloads/vcstools/",
+ keywords=["scm", "vcs", "git", "svn", "hg", "bzr"],
+ classifiers=["Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: BSD License"],
+ description="VCS/SCM source control library for svn, git, hg, and bzr",
+ long_description="""\
+Library for managing source code trees from multiple version control systems.
+Current supports svn, git, hg, and bzr.
+""",
+ license="BSD")
diff --git a/setup.sh b/setup.sh
new file mode 100644
index 0000000..14d4b2f
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,10 @@
+SCRIPT_PATH="${BASH_SOURCE[0]}";
+if([ -h "${SCRIPT_PATH}" ]) then
+ while([ -h "${SCRIPT_PATH}" ]) do SCRIPT_PATH=`readlink "${SCRIPT_PATH}"`; done
+fi
+pushd . > /dev/null
+cd `dirname ${SCRIPT_PATH}` > /dev/null
+SCRIPT_PATH=`pwd`;
+popd > /dev/null
+
+export PYTHONPATH=$SCRIPT_PATH/src:$PYTHONPATH
diff --git a/src/vcstools/__init__.py b/src/vcstools/__init__.py
new file mode 100644
index 0000000..fa2f801
--- /dev/null
+++ b/src/vcstools/__init__.py
@@ -0,0 +1,76 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+"""
+Library for tools that need to interact with ROS-support
+version control systems.
+"""
+
+from __future__ import absolute_import, print_function, unicode_literals
+import logging
+
+from vcstools.vcs_abstraction import VcsClient, VCSClient, register_vcs, \
+ get_vcs_client
+
+from vcstools.svn import SvnClient
+from vcstools.bzr import BzrClient
+from vcstools.hg import HgClient
+from vcstools.git import GitClient
+from vcstools.tar import TarClient
+
+# configure the VCSClient
+register_vcs("svn", SvnClient)
+register_vcs("bzr", BzrClient)
+register_vcs("git", GitClient)
+register_vcs("hg", HgClient)
+register_vcs("tar", TarClient)
+
+
+def setup_logger():
+ """
+ creates a logger 'vcstools'
+ """
+ logger = logging.getLogger('vcstools')
+ logger.setLevel(logging.WARN)
+ handler = logging.StreamHandler()
+ handler.setLevel(logging.WARN)
+
+ # create formatter
+ template = '%(levelname)s [%(name)s] %(message)s[/%(name)s]'
+ formatter = logging.Formatter(template)
+ # add formatter to handler
+ handler.setFormatter(formatter)
+
+ # add handler to logger
+ logger.addHandler(handler)
+
+setup_logger()
diff --git a/src/vcstools/__version__.py b/src/vcstools/__version__.py
new file mode 100644
index 0000000..41bf89f
--- /dev/null
+++ b/src/vcstools/__version__.py
@@ -0,0 +1 @@
+version = '0.1.32'
diff --git a/src/vcstools/bzr.py b/src/vcstools/bzr.py
new file mode 100644
index 0000000..b7fc412
--- /dev/null
+++ b/src/vcstools/bzr.py
@@ -0,0 +1,287 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+"""
+bzr vcs support.
+"""
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+
+import re
+import email.utils # For email parsing
+import dateutil.parser # Date string parsing
+
+# first try python3, then python2
+try:
+ from urllib.request import url2pathname
+except ImportError:
+ from urllib2 import url2pathname
+
+from vcstools.vcs_base import VcsClientBase, VcsError
+from vcstools.common import sanitized, normalized_rel_path, \
+ run_shell_command, ensure_dir_notexists
+
+
+def _get_bzr_version():
+ """Looks up bzr version by calling bzr --version.
+ :raises: VcsError if bzr is not installed"""
+ try:
+ value, output, _ = run_shell_command('bzr --version',
+ shell=True,
+ us_env=True)
+ if value == 0 and output is not None and len(output.splitlines()) > 0:
+ version = output.splitlines()[0]
+ else:
+ raise VcsError("bzr --version returned %s," +
+ " maybe bzr is not installed" %
+ value)
+ except VcsError as e:
+ raise VcsError("Coud not determine whether bzr is installed: %s" % e)
+ return version
+
+
+class BzrClient(VcsClientBase):
+ def __init__(self, path):
+ """
+ :raises: VcsError if bzr not detected
+ """
+ VcsClientBase.__init__(self, 'bzr', path)
+ _get_bzr_version()
+
+ @staticmethod
+ def get_environment_metadata():
+ metadict = {}
+ try:
+ metadict["version"] = _get_bzr_version()
+ except:
+ metadict["version"] = "no bzr installed"
+ return metadict
+
+ def get_url(self):
+ """
+ :returns: BZR URL of the branch (output of bzr info command),
+ or None if it cannot be determined
+ """
+ result = None
+ if self.detect_presence():
+ cmd = 'bzr info %s' % self._path
+ _, output, _ = run_shell_command(cmd, shell=True, us_env=True)
+ matches = [l for l in output.splitlines() if l.startswith(' parent branch: ')]
+ if matches:
+ ppath = url2pathname(matches[0][len(' parent branch: '):])
+ # when it can, bzr substitues absolute path for relative paths
+ if (ppath is not None and os.path.isdir(ppath) and not os.path.isabs(ppath)):
+ result = os.path.abspath(os.path.join(os.getcwd(), ppath))
+ else:
+ result = ppath
+ return result
+
+ def url_matches(self, url, url_or_shortcut):
+ if super(BzrClient, self).url_matches(url, url_or_shortcut):
+ return True
+ # if we got a shortcut (e.g. launchpad url), we compare using
+ # bzr info and return that one if result matches.
+ result = False
+ if url_or_shortcut is not None:
+ cmd = 'bzr info %s' % url_or_shortcut
+ value, output, _ = run_shell_command(cmd, shell=True, us_env=True)
+ if value == 0:
+ for line in output.splitlines():
+ sline = line.strip()
+ for prefix in ['shared repository: ',
+ 'repository branch: ',
+ 'branch root: ']:
+ if sline.startswith(prefix):
+ if super(BzrClient, self).url_matches(url, sline[len(prefix):]):
+ result = True
+ break
+ return result
+
+ def detect_presence(self):
+ return self.path_exists() and os.path.isdir(os.path.join(self._path, '.bzr'))
+
+ def checkout(self, url, version=None, verbose=False, shallow=False):
+ if url is None or url.strip() == '':
+ raise ValueError('Invalid empty url : "%s"' % url)
+ # bzr 2.5.1 fails if empty directory exists
+ if not ensure_dir_notexists(self.get_path()):
+ self.logger.error("Can't remove %s" % self.get_path())
+ return False
+ cmd = 'bzr branch'
+ if version:
+ cmd += ' -r %s' % version
+ cmd += ' %s %s' % (url, self._path)
+ value, _, msg = run_shell_command(cmd,
+ shell=True,
+ show_stdout=verbose,
+ verbose=verbose)
+ if value != 0:
+ if msg:
+ self.logger.error('%s' % msg)
+ return False
+ return True
+
+ def update(self, version='', verbose=False):
+ if not self.detect_presence():
+ return False
+ value, _, _ = run_shell_command("bzr pull",
+ cwd=self._path,
+ shell=True,
+ show_stdout=True,
+ verbose=verbose)
+ if value != 0:
+ return False
+ # Ignore verbose param, bzr is pretty verbose on update anyway
+ if version is not None and version != '':
+ cmd = "bzr update -r %s" % (version)
+ else:
+ cmd = "bzr update"
+ value, _, _ = run_shell_command(cmd,
+ cwd=self._path,
+ shell=True,
+ show_stdout=True,
+ verbose=verbose)
+ if value == 0:
+ return True
+ return False
+
+ def get_version(self, spec=None):
+ """
+ :param spec: (optional) revisionspec of desired version. May
+ be any revisionspec as returned by 'bzr help revisionspec',
+ e.g. a tagname or 'revno:<number>'
+ :returns: the current revision number of the repository. Or if
+ spec is provided, the number of a revision specified by some
+ token.
+ """
+ if self.detect_presence():
+ if spec is not None:
+ command = ['bzr log -r %s .' % sanitized(spec)]
+ _, output, _ = run_shell_command(command,
+ shell=True,
+ cwd=self._path,
+ us_env=True)
+ if output is None or output.strip() == '' or output.startswith("bzr:"):
+ return None
+ else:
+ matches = [l for l in output.split('\n') if l.startswith('revno: ')]
+ if len(matches) == 1:
+ return matches[0].split()[1]
+ else:
+ _, output, _ = run_shell_command('bzr revno --tree',
+ shell=True,
+ cwd=self._path,
+ us_env=True)
+ return output.strip()
+
+ def get_diff(self, basepath=None):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = sanitized(normalized_rel_path(self._path, basepath))
+ command = "bzr diff %s" % rel_path
+ command += " -p1 --prefix %s/:%s/" % (rel_path, rel_path)
+ _, response, _ = run_shell_command(command, shell=True, cwd=basepath)
+ return response
+
+ def get_log(self, relpath=None, limit=None):
+ response = []
+
+ if relpath is None:
+ relpath = ''
+
+ # Compile regexes
+ id_regex = re.compile('^revno: ([0-9]+)$', flags=re.MULTILINE)
+ committer_regex = re.compile('^committer: (.+)$', flags=re.MULTILINE)
+ timestamp_regex = re.compile('^timestamp: (.+)$', flags=re.MULTILINE)
+ message_regex = re.compile('^ (.+)$', flags=re.MULTILINE)
+
+ if self.path_exists() and os.path.exists(os.path.join(self._path, relpath)):
+ # Get the log
+ limit_cmd = (("--limit=%d" % (int(limit))) if limit else "")
+ command = "bzr log %s %s" % (sanitized(relpath), limit_cmd)
+ return_code, text_response, stderr = run_shell_command(command, shell=True, cwd=self._path)
+ if return_code == 0:
+ revno_match = id_regex.findall(text_response)
+ committer_match = committer_regex.findall(text_response)
+ timestamp_match = timestamp_regex.findall(text_response)
+ message_match = message_regex.findall(text_response)
+
+ # Extract the entries
+ for revno, committer, timestamp, message in zip(revno_match,
+ committer_match,
+ timestamp_match,
+ message_match):
+ author, email_address = email.utils.parseaddr(committer)
+ date = dateutil.parser.parse(timestamp)
+ log_data = {'id': revno,
+ 'author': author,
+ 'email': email_address,
+ 'message': message,
+ 'date': date}
+
+ response.append(log_data)
+
+ return response
+
+ def get_status(self, basepath=None, untracked=False):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ command = "bzr status %s -S" % sanitized(rel_path)
+ if not untracked:
+ command += " -V"
+ _, response, _ = run_shell_command(command, shell=True, cwd=basepath)
+ response_processed = ""
+ for line in response.split('\n'):
+ if len(line.strip()) > 0:
+ response_processed += line[0:4] + rel_path + '/'
+ response_processed += line[4:] + '\n'
+ response = response_processed
+ return response
+
+ def export_repository(self, version, basepath):
+ # execute the bzr export cmd
+ cmd = 'bzr export --format=tgz {0} '.format(basepath + '.tar.gz')
+ cmd += '{0}'.format(version)
+ result, _, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ if result:
+ return False
+ return True
+
+BZRClient = BzrClient
diff --git a/src/vcstools/common.py b/src/vcstools/common.py
new file mode 100644
index 0000000..c753fa1
--- /dev/null
+++ b/src/vcstools/common.py
@@ -0,0 +1,314 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+import errno
+import os
+import copy
+import shlex
+import subprocess
+import logging
+import netrc
+import tempfile
+import shutil
+
+try:
+ # py3k
+ from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, \
+ HTTPBasicAuthHandler, build_opener
+ from urllib.parse import urlparse
+except ImportError:
+ # py2.7
+ from urlparse import urlparse
+ from urllib2 import urlopen, HTTPPasswordMgrWithDefaultRealm, \
+ HTTPBasicAuthHandler, build_opener
+
+from vcstools.vcs_base import VcsError
+
+
+def ensure_dir_notexists(path):
+ """
+ helper function, removes dir if it exists
+
+ :returns: True if dir does not exist after this function
+ :raises: OSError if dir exists and removal failed for non-trivial reasons
+ """
+ try:
+ if os.path.exists(path):
+ os.rmdir(path)
+ return True
+ except OSError as ose:
+ # ignore if directory
+ if not ose.errno in [errno.ENOENT, errno.ENOTEMPTY, errno.ENOTDIR]:
+ return False
+
+
+def urlopen_netrc(uri, *args, **kwargs):
+ '''
+ wrapper to urlopen, using netrc on 401 as fallback
+ Since this wraps both python2 and python3 urlopen, accepted arguments vary
+
+ :returns: file-like object as urllib.urlopen
+ :raises: IOError and urlopen errors
+ '''
+ try:
+ return urlopen(uri, *args, **kwargs)
+ except IOError as ioe:
+ if hasattr(ioe, 'code') and ioe.code == 401:
+ # 401 means authentication required, we try netrc credentials
+ result = _netrc_open(uri)
+ if result is not None:
+ return result
+ raise
+
+
+def urlretrieve_netrc(url, filename=None):
+ '''
+ writes a temporary file with the contents of url. This works
+ similar to urllib2.urlretrieve, but uses netrc as fallback on 401,
+ and has no reporthook or data option. Also urllib2.urlretrieve
+ malfunctions behind proxy, so we avoid it.
+
+ :param url: What to retrieve
+ :param filename: target file (default is basename of url)
+ :returns: (filename, response_headers)
+ :raises: IOError and urlopen errors
+ '''
+ fname = None
+ fhand = None
+ try:
+ resp = urlopen_netrc(url)
+ if filename:
+ fhand = open(filename, 'wb')
+ fname = filename
+ else:
+ # Make a temporary file
+ fdesc, fname = tempfile.mkstemp()
+ fhand = os.fdopen(fdesc, "wb")
+ # Copy the http response to the temporary file.
+ shutil.copyfileobj(resp.fp, fhand)
+ finally:
+ if fhand:
+ fhand.close()
+ return (fname, resp.headers)
+
+
+def _netrc_open(uri, filename=None):
+ '''
+ open uri using netrc credentials.
+
+ :param uri: uri to open
+ :param filename: optional, path to non-default netrc config file
+ :returns: file-like object from opening a socket to uri, or None
+ :raises IOError: if opening .netrc file fails (unless file not found)
+ '''
+ if not uri:
+ return None
+ parsed_uri = urlparse(uri)
+ machine = parsed_uri.netloc
+ if not machine:
+ return None
+ opener = None
+ try:
+ info = netrc.netrc(filename).authenticators(machine)
+ if info is not None:
+ (username, _, password) = info
+ if username and password:
+ pass_man = HTTPPasswordMgrWithDefaultRealm()
+ pass_man.add_password(None, machine, username, password)
+ authhandler = HTTPBasicAuthHandler(pass_man)
+ opener = build_opener(authhandler)
+ return opener.open(uri)
+ else:
+ # caught below, like other netrc parse errors
+ raise netrc.NetrcParseError('No authenticators for "%s"' % machine)
+ except IOError as ioe:
+ if ioe.errno != 2:
+ # if = 2, User probably has no .netrc, this is not an error
+ raise
+ except netrc.NetrcParseError as neterr:
+ logger = logging.getLogger('vcstools')
+ logger.warn('WARNING: parsing .netrc: %s' % str(neterr))
+ # we could install_opener() here, but prefer to keep
+ # default opening clean. Client can do that, though.
+ return None
+
+
+def normalized_rel_path(path, basepath):
+ """
+ If path is absolute, return relative path to it from
+ basepath. If relative, return it normalized.
+
+ :param path: an absolute or relative path
+ :param basepath: if path is absolute, shall be made relative to this
+ :returns: a normalized relative path
+ """
+ # gracefully ignore invalid input absolute path + no basepath
+ if path is None:
+ return basepath
+ if os.path.isabs(path) and basepath is not None:
+ return os.path.normpath(os.path.relpath(os.path.realpath(path), os.path.realpath(basepath)))
+ return os.path.normpath(path)
+
+
+def sanitized(arg):
+ """
+ makes sure a composed command to be executed via shell was not injected.
+
+ A composed command would be like "ls %s"%foo.
+ In this example, foo could be "; rm -rf *"
+ sanitized raises an Error when it detects such an attempt
+
+ :raises VcsError: on injection attempts
+ """
+ if arg is None or arg.strip() == '':
+ return ''
+ arg = str(arg.strip('"').strip())
+ safe_arg = '"%s"' % arg
+ # this also detects some false positives, like bar"";foo
+ if '"' in arg:
+ if (len(shlex.split(safe_arg, False, False)) != 1):
+ raise VcsError("Shell injection attempt detected: >%s< = %s" %
+ (arg, shlex.split(safe_arg, False, False)))
+ return safe_arg
+
+
+def _discard_line(line):
+ if line is None:
+ return True
+ # the most common feedback lines of scms. We don't care about those. We let through anything unusual only.
+ discard_prefixes = ["adding ", "added ", "updating ", "requesting ", "pulling from ",
+ "searching for ", "(", "no changes found",
+ "0 files",
+ "A ", "D ", "U ",
+ "At revision", "Path: ", "First,",
+ "Installing", "Using ",
+ "No ", "Tree ",
+ "All ",
+ "+N ", "-D ", " M ", " M* ", "RM" # bzr
+ ]
+ for pre in discard_prefixes:
+ if line.startswith(pre):
+ return True
+ return False
+
+
+def run_shell_command(cmd, cwd=None, shell=False, us_env=True,
+ show_stdout=False, verbose=False,
+ no_warn=False, no_filter=False):
+ """
+ executes a command and hides the stdout output, loggs stderr
+ output when command result is not zero. Make sure to sanitize
+ arguments in the command.
+
+ :param cmd: A string to execute.
+ :param shell: Whether to use os shell.
+ :param us_env: changes env var LANG before running command, can influence program output
+ :param show_stdout: show some of the output (except for discarded lines in _discard_line()), ignored if no_filter
+ :param no_warn: hides warnings
+ :param verbose: show all output, overrides no_warn, ignored if no_filter
+ :param no_filter: does not wrap stdout, so invoked command prints everything outside our knowledge
+ this is DANGEROUS, as vulnerable to shell injection.
+ :returns: ( returncode, stdout, stderr); stdout is None if no_filter==True
+ :raises: VcsError on OSError
+ """
+ try:
+ env = copy.copy(os.environ)
+ if us_env:
+ env["LANG"] = "en_US.UTF-8"
+ if no_filter:
+ # in no_filter mode, we cannot pipe stdin, as this
+ # causes some prompts to be hidden (e.g. mercurial over
+ # http)
+ stdout_target = None
+ stderr_target = None
+ else:
+ stdout_target = subprocess.PIPE
+ stderr_target = subprocess.PIPE
+ proc = subprocess.Popen(cmd,
+ shell=shell,
+ cwd=cwd,
+ stdout=stdout_target,
+ stderr=stderr_target,
+ env=env)
+ # when we read output in while loop, it would not be returned
+ # in communicate()
+ stdout_buf = []
+ stderr_buf = []
+ if not no_filter:
+ if (verbose or show_stdout):
+ # this loop runs until proc is done
+ # it listen to the pipe, print and stores result in buffer for returning
+ # this allows proc to run while we still can filter out output
+ # avoiding readline() because it may block forever
+ for line in iter(proc.stdout.readline, b''):
+ line = line.decode('UTF-8')
+ if line is not None and line != '':
+ if verbose or not _discard_line(line):
+ print(line),
+ stdout_buf.append(line)
+ if (not line or proc.returncode is not None):
+ break
+ # stderr was swallowed in pipe, in verbose mode print lines
+ if verbose:
+ for line in iter(proc.stderr.readline, b''):
+ line = line.decode('UTF-8')
+ if line != '':
+ print(line),
+ stderr_buf.append(line)
+ if not line:
+ break
+
+ (stdout, stderr) = proc.communicate()
+ if stdout is not None:
+ stdout_buf.append(stdout.decode('utf-8'))
+ stdout = "\n".join(stdout_buf)
+ if stderr is not None:
+ stderr_buf.append(stderr.decode('utf-8'))
+ stderr = "\n".join(stderr_buf)
+ message = None
+ if proc.returncode != 0 and stderr is not None and stderr != '':
+ logger = logging.getLogger('vcstools')
+ message = "Command failed: '%s'" % (cmd)
+ if cwd is not None:
+ message += "\n run at: '%s'" % (cwd)
+ message += "\n errcode: %s:\n%s" % (proc.returncode, stderr)
+ logger.warn(message)
+ result = stdout
+ if result is not None:
+ result = result.rstrip()
+ return (proc.returncode, result, message)
+ except OSError as ose:
+ logger = logging.getLogger('vcstools')
+ message = "Command failed with OSError. '%s' <%s, %s>:\n%s" % (cmd, shell, cwd, ose)
+ logger.error(message)
+ raise VcsError(message)
diff --git a/src/vcstools/git.py b/src/vcstools/git.py
new file mode 100644
index 0000000..760bd15
--- /dev/null
+++ b/src/vcstools/git.py
@@ -0,0 +1,759 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+"""
+git vcs support.
+
+refnames in git can be branchnames, hashes, partial hashes, tags. On
+checkout, git will disambiguate by checking them in that order, taking
+the first that applies
+
+This class aims to provide git for linear centralized workflows. This
+means we assume that the only relevant remote is the one named
+"origin", and we assume that commits once on origin remain on origin.
+
+A challenge with git is that it has strong reasonable conventions, but
+is very allowing for breaking them. E.g. it is possible to name
+remotes and branches with names like "refs/heads/master", give
+branches and tags the same name, or a valid SHA-ID as name, etc.
+Similarly git allows plenty of ways to reference any object, in case
+of ambiguities, git attempts to take the most reasonable
+disambiguation, and in some cases warns.
+"""
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import gzip
+import dateutil.parser # For parsing date strings
+from distutils.version import LooseVersion
+
+from vcstools.vcs_base import VcsClientBase, VcsError
+from vcstools.common import sanitized, normalized_rel_path, run_shell_command
+
+
+class GitError(Exception):
+ pass
+
+
+def _git_diff_path_submodule_change(diff, rel_path_prefix):
+ """
+ Parses git diff result and changes the filename prefixes.
+ """
+ if diff is None:
+ return None
+ INIT = 0
+ INDIFF = 1
+ # small state machine makes sure we never touch anything inside
+ # the actual diff
+ state = INIT
+ result = ""
+ s_list = [line for line in diff.split(os.linesep)]
+ subrel_path = rel_path_prefix
+ for line in s_list:
+ newline = line
+ if line.startswith("Entering '"):
+ state = INIT
+ submodulepath = line.rstrip("'")[len("Entering '"):]
+ subrel_path = os.path.join(rel_path_prefix, submodulepath)
+ continue
+ if line.startswith("diff --git "):
+ state = INIT
+ if state == INIT:
+ if line.startswith("@@"):
+ state = INDIFF
+ else:
+ if line.startswith("---") and not line.startswith("--- /dev/null"):
+ newline = "--- " + subrel_path + line[5:]
+ if line.startswith("+++") and not line.startswith("+++ /dev/null"):
+ newline = "+++ " + subrel_path + line[5:]
+ if line.startswith("diff --git"):
+ # first replacing b in case path starts with a/
+ newline = line.replace(" b/", " " + subrel_path + "/", 1)
+ newline = newline.replace(" a/", " " + subrel_path + "/", 1)
+ if newline != '':
+ result += newline + '\n'
+ return result
+
+
+def _get_git_version():
+ """Looks up git version by calling git --version.
+
+ :raises: VcsError if git is not installed or returns
+ something unexpected"""
+ try:
+ cmd = 'git --version'
+ value, version, _ = run_shell_command(cmd, shell=True)
+ if value != 0:
+ raise VcsError("git --version returned %s, maybe git is not installed" % (value))
+ prefix = 'git version '
+ if version is not None and version.startswith(prefix):
+ version = version[len(prefix):].strip()
+ else:
+ raise VcsError("git --version returned invalid string: '%s'" % version)
+ except VcsError as exc:
+ raise VcsError("Could not determine whether git is installed: %s" % exc)
+ return version
+
+
+class GitClient(VcsClientBase):
+ def __init__(self, path):
+ """
+ :raises: VcsError if git not detected
+ """
+ VcsClientBase.__init__(self, 'git', path)
+ self.gitversion = _get_git_version()
+
+ @staticmethod
+ def get_environment_metadata():
+ metadict = {}
+ try:
+ version = _get_git_version()
+ resetkeep = LooseVersion(version) >= LooseVersion('1.7.1')
+ submodules = LooseVersion(version) > LooseVersion('1.7')
+ metadict["features"] = "'reset --keep': %s, submodules: %s" % (resetkeep, submodules)
+ except VcsError:
+ version = "No git installed"
+ metadict["version"] = version
+ return metadict
+
+ def get_url(self):
+ """
+ :returns: GIT URL of the directory path (output of git info command), or None if it cannot be determined
+ """
+ if self.detect_presence():
+ cmd = "git config --get remote.origin.url"
+ _, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ return output.rstrip()
+ return None
+
+ def detect_presence(self):
+ # There is a proposed implementation of detect_presence which might be
+ # more future proof, but would depend on parsing the output of git
+ # See: https://github.com/vcstools/vcstools/pull/10
+ return self.path_exists() and os.path.exists(os.path.join(self._path, '.git'))
+
+ def checkout(self, url, version=None, verbose=False, shallow=False):
+ """calls git clone and then, if version was given, update(version)"""
+ if url is None or url.strip() == '':
+ raise ValueError('Invalid empty url : "%s"' % url)
+
+ #since we cannot know whether version names a branch, clone master initially
+ cmd = 'git clone'
+ if shallow:
+ cmd += ' --depth 1'
+ if LooseVersion(self.gitversion) >= LooseVersion('1.7.10'):
+ cmd += ' --no-single-branch'
+ if version is None:
+ # quicker than using _do_update, but undesired when switching branches next
+ cmd += ' --recursive'
+ cmd += ' %s %s' % (url, self._path)
+ value, _, msg = run_shell_command(cmd,
+ shell=True,
+ no_filter=True,
+ show_stdout=verbose,
+ verbose=verbose)
+ if value != 0:
+ if msg:
+ self.logger.error('%s' % msg)
+ return False
+
+ try:
+ # update to make sure we are on the right branch. Do not
+ # check for "master" here, as default branch could be anything
+ if version is not None:
+ return self._do_update(version,
+ verbose=verbose,
+ fast_foward=True,
+ update_submodules=True)
+ else:
+ return True
+ except GitError:
+ return False
+
+ def update_submodules(self, verbose=False):
+
+ # update and or init submodules too
+ if LooseVersion(self.gitversion) > LooseVersion('1.7'):
+ cmd = "git submodule update --init --recursive"
+ value, _, _ = run_shell_command(cmd,
+ shell=True,
+ cwd=self._path,
+ show_stdout=True,
+ verbose=verbose)
+ if value != 0:
+ return False
+ return True
+
+ def update(self, version=None, verbose=False, force_fetch=False):
+ """
+ if version is None, attempts fast-forwarding current branch, if any.
+
+ Else interprets version as a local branch, remote branch, tagname,
+ hash, etc.
+
+ If it is a branch, attempts to move to it unless
+ already on it, and to fast-forward, unless not a tracking
+ branch. Else go untracked on tag or whatever version is. Does
+ not leave if current commit would become dangling.
+
+ :return: True if already up-to-date with remote or after successful fast_foward
+ """
+ if not self.detect_presence():
+ return False
+
+ try:
+ # fetch in any case to get updated tags even if we don't need them
+ self._do_fetch()
+ return self._do_update(refname=version, verbose=verbose)
+ except GitError:
+ return False
+
+ def _do_update(self,
+ refname=None,
+ verbose=False,
+ fast_foward=True,
+ update_submodules=True):
+ '''
+ updates without fetching, thus any necessary fetching must be done before
+ allows arguments to reduce unnecessary steps after checkout
+
+ :param fast_foward: if false, does not perform fast-forward
+ :param update_submodules: if false, does not attempt to update submodules
+ '''
+ # are we on any branch?
+ current_branch = self.get_branch()
+ branch_parent = None
+ if current_branch:
+ # local branch might be named differently from remote by user, we respect that
+ same_branch = (refname == current_branch)
+ if not same_branch:
+ branch_parent = self.get_branch_parent(current_branch=current_branch)
+ if not refname:
+ # ! changing refname to cause fast-forward
+ refname = branch_parent
+ same_branch = True
+ else:
+ same_branch = (refname == branch_parent)
+ if not branch_parent:
+ # avoid checking branch parent again later
+ fast_foward = False
+ else:
+ same_branch = False
+
+ if not refname:
+ # we are neither tracking, nor did we get any refname to update to
+ return (not update_submodules) or self.update_submodules(verbose=verbose)
+
+ if same_branch:
+ if fast_foward:
+ if not branch_parent and current_branch:
+ branch_parent = self.get_branch_parent(current_branch=current_branch)
+ # already on correct branch, fast-forward if there is a parent
+ if branch_parent:
+ if not self._do_fast_forward(branch_parent=branch_parent,
+ fetch=False,
+ verbose=verbose):
+ return False
+ else:
+ # refname can be a different branch or something else than a branch
+
+ refname_is_local_branch = self.is_local_branch(refname)
+ if refname_is_local_branch:
+ # might also be remote branch, but we treat it as local
+ refname_is_remote_branch = False
+ else:
+ refname_is_remote_branch = self.is_remote_branch(refname, fetch=False)
+ refname_is_branch = refname_is_remote_branch or refname_is_local_branch
+
+ current_version = None
+ # shortcut if version is the same as requested
+ if not refname_is_branch:
+ current_version = self.get_version()
+ if current_version == refname:
+ return (not update_submodules) or self.update_submodules(verbose=verbose)
+
+ if current_branch is None:
+ if not current_version:
+ current_version = self.get_version()
+ # prevent commit from becoming dangling
+ if self.is_commit_in_orphaned_subtree(current_version, fetch=False):
+ # commit becomes dangling unless we move to one of its descendants
+ if not self.rev_list_contains(refname, current_version, fetch=False):
+ # TODO: should raise error instead of printing message
+ print("vcstools refusing to move away from dangling commit, to protect your work.")
+ return False
+
+ # git checkout makes all the decisions for us
+ self._do_checkout(refname, verbose=verbose, fetch=False)
+
+ if refname_is_local_branch:
+ # if we just switched to a local tracking branch (not created one), we should also fast forward
+ new_branch_parent = self.get_branch_parent(current_branch=refname)
+ if new_branch_parent is not None:
+ if fast_foward:
+ if not self._do_fast_forward(branch_parent=new_branch_parent,
+ fetch=False,
+ verbose=verbose):
+ return False
+ return (not update_submodules) or self.update_submodules(verbose=verbose)
+
+ def get_version(self, spec=None):
+ """
+ :param spec: (optional) token to identify desired version. For
+ git, this may be anything accepted by git log, e.g. a tagname,
+ branchname, or sha-id.
+ :param fetch: When spec is given, can be used to suppress git fetch call
+ :returns: current SHA-ID of the repository. Or if spec is
+ provided, the SHA-ID of a commit specified by some token if found, else None
+ """
+ if self.detect_presence():
+ command = "git log -1"
+ if spec is not None:
+ command += " %s" % sanitized(spec)
+ command += " --format='%H'"
+ output = ''
+ #we repeat the call once after fetching if necessary
+ for _ in range(2):
+ _, output, _ = run_shell_command(command,
+ shell=True,
+ cwd=self._path)
+ if (output != '' or spec is None):
+ break
+ # we try again after fetching if given spec had not been found
+ try:
+ self._do_fetch()
+ except GitError:
+ return None
+ # On Windows the version can have single quotes around it
+ output = output.strip("'")
+ return output
+ return None
+
+ def get_diff(self, basepath=None):
+ response = ''
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ # git needs special treatment as it only works from inside
+ # use HEAD to also show staged changes. Maybe should be option?
+ # injection should be impossible using relpath, but to be sure, we check
+ cmd = "git diff HEAD --src-prefix=%s/ --dst-prefix=%s/ ." % \
+ (sanitized(rel_path), sanitized(rel_path))
+ _, response, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ if LooseVersion(self.gitversion) > LooseVersion('1.7'):
+ cmd = 'git submodule foreach --recursive git diff HEAD'
+ _, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ response += _git_diff_path_submodule_change(output, rel_path)
+ return response
+
+ def get_log(self, relpath=None, limit=None):
+ response = []
+
+ if relpath is None:
+ relpath = ''
+
+ if self.path_exists() and os.path.exists(os.path.join(self._path, relpath)):
+ # Get the log
+ limit_cmd = (("-n %d" % (int(limit))) if limit else "")
+
+ GIT_COMMIT_FIELDS = ['id', 'author', 'email', 'date', 'message']
+ GIT_LOG_FORMAT = '%x1f'.join(['%H', '%an', '%ae', '%ad', '%s']) + '%x1e'
+
+ command = "git --work-tree=%s log --format=\"%s\" %s %s " % (self._path, GIT_LOG_FORMAT,
+ limit_cmd, sanitized(relpath))
+ return_code, response_str, stderr = run_shell_command(command, shell=True, cwd=self._path)
+
+ if return_code == 0:
+ # Parse response
+ response = response_str.strip('\n\x1e').split("\x1e")
+ response = [row.strip().split("\x1f") for row in response]
+ response = [dict(zip(GIT_COMMIT_FIELDS, row)) for row in response]
+
+ # Parse dates
+ for entry in response:
+ entry['date'] = dateutil.parser.parse(entry['date'])
+
+ return response
+
+ def get_status(self, basepath=None, untracked=False):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ # git command only works inside repo
+ # self._path is safe against command injection, as long as we check path.exists
+ command = "git status -s "
+ if not untracked:
+ command += " -uno"
+ _, response, _ = run_shell_command(command,
+ shell=True,
+ cwd=self._path)
+ response_processed = ""
+ for line in response.split('\n'):
+ if len(line.strip()) > 0:
+ # prepend relative path
+ response_processed += '%s%s/%s\n' % (line[0:3],
+ rel_path,
+ line[3:])
+ if LooseVersion(self.gitversion) > LooseVersion('1.7'):
+ command = "git submodule foreach --recursive git status -s"
+ if not untracked:
+ command += " -uno"
+ _, response2, _ = run_shell_command(command,
+ shell=True,
+ cwd=self._path)
+ for line in response2.split('\n'):
+ if line.startswith("Entering"):
+ continue
+ if len(line.strip()) > 0:
+ # prepend relative path
+ response_processed += line[0:3] + rel_path + '/' + line[3:] + '\n'
+ response = response_processed
+ return response
+
+ def is_remote_branch(self, branch_name, fetch=True):
+ """
+ checks list of remote branches for match. Set fetch to False if you just fetched already.
+
+ :returns: True if git branch knows ref for remote "origin"
+ :raises: GitError when git fetch fails
+ """
+ if self.path_exists():
+ if fetch:
+ self._do_fetch()
+ _, output, _ = run_shell_command('git branch -r',
+ shell=True,
+ cwd=self._path)
+ for l in output.splitlines():
+ elem = l.split()[0]
+ rem_name = elem[:elem.find('/')]
+ br_name = elem[elem.find('/') + 1:]
+ if rem_name == "origin" and br_name == branch_name:
+ return True
+ return False
+
+ def is_local_branch(self, branch_name):
+ if self.path_exists():
+ _, output, _ = run_shell_command('git branch',
+ shell=True,
+ cwd=self._path)
+ for line in output.splitlines():
+ elems = line.split()
+ if len(elems) == 1:
+ if elems[0] == branch_name:
+ return True
+ elif len(elems) == 2:
+ if elems[0] == '*' and elems[1] == branch_name:
+ return True
+ return False
+
+ def get_branch(self):
+ if self.path_exists():
+ _, output, _ = run_shell_command('git branch',
+ shell=True,
+ cwd=self._path)
+ for line in output.splitlines():
+ elems = line.split()
+ if len(elems) == 2 and elems[0] == '*':
+ return elems[1]
+ return None
+
+ def get_branch_parent(self, fetch=False, current_branch=None):
+ """
+ return the name of the branch this branch tracks, if any
+
+ :raises: GitError if fetch fails
+ """
+ if self.path_exists():
+ # get name of configured merge ref.
+ branchname = current_branch or self.get_branch()
+ if branchname is None:
+ return None
+ cmd = 'git config --get %s' % sanitized('branch.%s.merge' % branchname)
+
+ _, output, _ = run_shell_command(cmd,
+ shell=True,
+ cwd=self._path)
+ if not output:
+ return None
+ lines = output.splitlines()
+ if len(lines) > 1:
+ print("vcstools unable to handle multiple merge references for branch %s:\n%s" % (branchname, output))
+ return None
+ # get name of configured remote
+ cmd = 'git config --get "branch.%s.remote"' % branchname
+ _, output2, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ if output2 != "origin":
+ print("vcstools only handles branches tracking remote 'origin'," +
+ " branch '%s' tracks remote '%s'" % (branchname, output2))
+ return None
+ output = lines[0]
+ # output is either refname, or /refs/heads/refname, or
+ # heads/refname we would like to return refname however,
+ # user could also have named any branch
+ # "/refs/heads/refname", for some unholy reason check all
+ # known branches on remote for refname, then for the odd
+ # cases, as git seems to do
+ candidate = output
+ if candidate.startswith('refs/'):
+ candidate = candidate[len('refs/'):]
+ if candidate.startswith('heads/'):
+ candidate = candidate[len('heads/'):]
+ elif candidate.startswith('tags/'):
+ candidate = candidate[len('tags/'):]
+ elif candidate.startswith('remotes/'):
+ candidate = candidate[len('remotes/'):]
+ if self.is_remote_branch(candidate, fetch=fetch):
+ return candidate
+ if output != candidate and self.is_remote_branch(output, fetch=False):
+ return output
+ return None
+
+ def is_tag(self, tag_name, fetch=True):
+ """
+ checks list of tags for match.
+ Set fetch to False if you just fetched already.
+
+ :returns: True if tag_name among known tags
+ :raises: GitError when call to git fetch fails
+ """
+ if fetch:
+ self._do_fetch()
+ if not tag_name:
+ raise ValueError('is_tag requires tag_name, got: "%s"' % tag_name)
+ if self.path_exists():
+ cmd = 'git tag -l %s' % sanitized(tag_name)
+ _, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ lines = output.splitlines()
+ if len(lines) == 1:
+ return True
+ return False
+
+ def rev_list_contains(self, refname, version, fetch=True):
+ """
+ calls git rev-list with refname and returns True if version
+ can be found in rev-list result
+
+ :param refname: a git refname
+ :param version: an SHA IDs (if partial, caller is responsible
+ for mismatch)
+ :returns: True if version is an ancestor commit from refname
+ :raises: GitError when call to git fetch fails
+ """
+ # to avoid listing unnecessarily many rev-ids, we cut off all
+ # those we are definitely not interested in
+ # $ git rev-list foo bar ^baz ^bez
+ # means "list all the commits which are reachable from foo or
+ # bar, but not from baz or bez". We use --parents because
+ # ^baz also excludes baz itself. We could also use git
+ # show --format=%P to get all parents first and use that,
+ # not sure what's more performant
+ if fetch:
+ self._do_fetch()
+ if (refname is not None and refname != '' and
+ version is not None and version != ''):
+
+ cmd = 'git rev-list %s ^%s --parents' % (sanitized(refname), sanitized(version))
+ _, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ for line in output.splitlines():
+ # can have 1, 2 or 3 elements (commit, parent1, parent2)
+ for hashid in line.split(" "):
+ if hashid.startswith(version):
+ return True
+ return False
+
+ def is_commit_in_orphaned_subtree(self, version, mask_self=False, fetch=True):
+ """
+ checks git log --all (the list of all commits reached by
+ references, meaning branches or tags) for version. If it shows
+ up, that means git garbage collection will not remove the
+ commit. Else it would eventually be deleted.
+
+ :param version: SHA IDs (if partial, caller is responsible for mismatch)
+ :param mask_self: whether to consider direct references to this commit
+ (rather than only references on descendants) as well
+ :param fetch: whether fetch should be done first for remote refs
+ :returns: True if version is not recursively referenced by a branch or tag
+ :raises: GitError if git fetch fails
+ """
+ if fetch:
+ self._do_fetch()
+ if version is not None and version != '':
+ cmd = 'git show-ref -s'
+ _, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ refs = output.splitlines()
+ # 2000 seems like a number the linux shell can cope with
+ chunksize = 2000
+ refchunks = [refs[x:x + chunksize] for x in range(0, len(refs), chunksize)]
+ for refchunk in refchunks:
+ # git log over all refs except HEAD
+ cmd = 'git log ' + " ".join(refchunk)
+ if mask_self:
+ # %P: parent hashes
+ cmd += " --pretty=format:%P"
+ else:
+ # %H: commit hash
+ cmd += " --pretty=format:%H"
+ _, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ for line in output.splitlines():
+ if line.strip("'").startswith(version):
+ return False
+ return True
+ return False
+
+ def export_repository(self, version, basepath):
+ # Use the git archive function
+ cmd = "git archive -o {0}.tar {1}".format(basepath, version)
+ result, _, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ if result:
+ return False
+ try:
+ # Gzip the tar file
+ with open(basepath + '.tar', 'rb') as tar_file:
+ gzip_file = gzip.open(basepath + '.tar.gz', 'wb')
+ try:
+ gzip_file.writelines(tar_file)
+ finally:
+ gzip_file.close()
+ finally:
+ # Clean up
+ os.remove(basepath + '.tar')
+ return True
+
+ def _do_fetch(self):
+ """
+ calls git fetch
+ :raises: GitError when call fails
+ """
+ cmd = "git fetch"
+ value1, _, _ = run_shell_command(cmd,
+ cwd=self._path,
+ shell=True,
+ no_filter=True,
+ show_stdout=True)
+ ## git fetch --tags ONLY fetches new tags and commits used, no other commits!
+ cmd = "git fetch --tags"
+ value2, _, _ = run_shell_command(cmd,
+ cwd=self._path,
+ shell=True,
+ no_filter=True,
+ show_stdout=True)
+ if value1 != 0 or value2 != 0:
+ raise GitError('git fetch failed')
+
+ def _do_fast_forward(self, fetch=True, branch_parent=None, verbose=False):
+ """Execute git fetch if necessary, and if we can fast-foward,
+ do so to the last fetched version using git rebase.
+
+ :param branch_parent: name of branch we track
+ :param fetch: whether fetch should be done first for remote refs
+ :returns: True if up-to-date or after succesful fast-forward
+ :raises: GitError when git fetch fails
+ """
+ assert branch_parent is not None
+ current_version = self.get_version()
+ parent_version = self.get_version("remotes/origin/%s" % branch_parent)
+ if current_version == parent_version:
+ return True
+ # check if we are true ancestor of tracked branch
+ if not self.rev_list_contains(parent_version,
+ current_version,
+ fetch=fetch):
+ # if not rev_list_contains this version, we are on same
+ # commit (checked before), have advanced, or have diverged.
+ # Now check whether tracked branch is a true ancestor of us
+ if self.rev_list_contains(current_version,
+ parent_version,
+ fetch=False):
+ return True
+ return False
+ if verbose:
+ print("Rebasing repository")
+ # Rebase, do not pull, because somebody could have
+ # commited in the meantime.
+ if LooseVersion(self.gitversion) >= LooseVersion('1.7.1'):
+ # --keep allows o rebase even with local changes, as long as
+ # local changes are not in files that change between versions
+ cmd = "git reset --keep remotes/origin/%s" % branch_parent
+ value, _, _ = run_shell_command(cmd,
+ shell=True,
+ cwd=self._path,
+ show_stdout=True,
+ verbose=verbose)
+ if value == 0:
+ return True
+ else:
+ verboseflag = ''
+ if verbose:
+ verboseflag = '-v'
+ # prior to version 1.7.1, git does not know --keep
+ # Do not merge, rebase does nothing when there are local changes
+ cmd = "git rebase %s remotes/origin/%s" % (verboseflag, branch_parent)
+ value, _, _ = run_shell_command(cmd,
+ shell=True,
+ cwd=self._path,
+ show_stdout=True,
+ verbose=verbose)
+ if value == 0:
+ return True
+ return False
+
+ def _do_checkout(self, refname, fetch=True, verbose=False):
+ """
+ meaning git checkout, not vcstools checkout. This works
+ for local branches, remote branches, tagnames, hashes, etc.
+ git will create local branch of same name when no such local
+ branch exists, and also setup tracking. Git decides with own
+ rules whether local changes would cause conflicts, and refuses
+ to checkout else.
+
+ :raises GitError: when checkout fails
+ """
+ # since refname may relate to remote branch / tag we do not
+ # know about yet, do fetch if not already done
+ if fetch:
+ self._do_fetch()
+ cmd = "git checkout %s" % (refname)
+ value, _, _ = run_shell_command(cmd,
+ shell=True,
+ cwd=self._path,
+ show_stdout=verbose,
+ verbose=verbose)
+ if value != 0:
+ raise GitError('Git Checkout failed')
+
+
+#Backwards compatibility
+GITClient = GitClient
diff --git a/src/vcstools/hg.py b/src/vcstools/hg.py
new file mode 100644
index 0000000..e9aa415
--- /dev/null
+++ b/src/vcstools/hg.py
@@ -0,0 +1,328 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+"""
+hg vcs support.
+
+using ui object to redirect output into a string
+"""
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import sys
+
+import gzip
+
+import dateutil.parser # For parsing date strings
+
+from vcstools.vcs_base import VcsClientBase, VcsError
+from vcstools.common import sanitized, normalized_rel_path, run_shell_command
+
+
+def _get_hg_version():
+ """Looks up hg version by calling hg --version.
+ :raises: VcsError if hg is not installed"""
+ try:
+ value, output, _ = run_shell_command('hg --version',
+ shell=True,
+ us_env=True)
+ if value == 0 and output is not None and len(output.splitlines()) > 0:
+ version = output.splitlines()[0]
+ else:
+ raise VcsError("hg --version returned %s, output '%s', maybe hg is not installed" % (value, output))
+ except VcsError as e:
+ raise VcsError("Could not determine whether hg is installed %s" % e)
+ return version
+
+
+#hg diff cannot seem to be persuaded to accept a different prefix for filenames
+def _hg_diff_path_change(diff, path):
+ """
+ Parses hg diff result and changes the filename prefixes.
+ """
+ if diff is None:
+ return None
+ INIT = 0
+ INDIFF = 1
+ # small state machine makes sure we never touch anything inside
+ # the actual diff
+ state = INIT
+
+ s_list = [line for line in diff.split(os.linesep)]
+ lines = []
+ for line in s_list:
+ if line.startswith("diff"):
+ state = INIT
+ if state == INIT:
+ if line.startswith("@@"):
+ state = INDIFF
+ newline = line
+ else:
+ if line.startswith("---") and not line.startswith("--- /dev/null"):
+ newline = "--- %s%s" % (path, line[5:])
+ elif line.startswith("+++") and not line.startswith("+++ /dev/null"):
+ newline = "+++ %s%s" % (path, line[5:])
+ elif line.startswith("diff --git"):
+ # first replacing b in case path starts with a/
+ newline = line.replace(" b/", " " + path + "/", 1)
+ newline = newline.replace(" a/", " " + path + "/", 1)
+ else:
+ newline = line
+ else:
+ newline = line
+ if newline != '':
+ lines.append(newline)
+ result = "\n".join(lines)
+ return result
+
+
+class HgClient(VcsClientBase):
+
+ def __init__(self, path):
+ """
+ :raises: VcsError if hg not detected
+ """
+ VcsClientBase.__init__(self, 'hg', path)
+ _get_hg_version()
+
+ @staticmethod
+ def get_environment_metadata():
+ metadict = {}
+ try:
+ metadict["version"] = '%s' % _get_hg_version()
+ except:
+ metadict["version"] = "no mercurial installed"
+ return metadict
+
+ def get_url(self):
+ """
+ :returns: HG URL of the directory path. (output of hg paths
+ command), or None if it cannot be determined
+ """
+ if self.detect_presence():
+ cmd = "hg paths default"
+ _, output, _ = run_shell_command(cmd,
+ shell=True,
+ cwd=self._path,
+ us_env=True)
+ return output.rstrip()
+ return None
+
+ def detect_presence(self):
+ return (self.path_exists() and
+ os.path.isdir(os.path.join(self._path, '.hg')))
+
+ def checkout(self, url, version='', verbose=False, shallow=False):
+ if url is None or url.strip() == '':
+ raise ValueError('Invalid empty url : "%s"' % url)
+ # make sure that the parent directory exists for #3497
+ base_path = os.path.split(self.get_path())[0]
+ try:
+ os.makedirs(base_path)
+ except OSError:
+ # OSError thrown if directory already exists this is ok
+ pass
+ cmd = "hg clone %s %s" % (sanitized(url), self._path)
+ value, _, msg = run_shell_command(cmd,
+ shell=True,
+ no_filter=True)
+ if value != 0:
+ if msg:
+ sys.logger.error('%s' % msg)
+ return False
+ if version is not None and version.strip() != '':
+ cmd = "hg checkout %s" % sanitized(version)
+ value, _, msg = run_shell_command(cmd,
+ cwd=self._path,
+ shell=True,
+ no_filter=True)
+ if value != 0:
+ if msg:
+ sys.stderr.write('%s\n' % msg)
+ return False
+ return True
+
+ def update(self, version='', verbose=False):
+ verboseflag = ''
+ if verbose:
+ verboseflag = '--verbose'
+ if not self.detect_presence():
+ sys.stderr.write("Error: cannot update non-existing directory\n")
+ return True
+ if not self._do_pull():
+ return False
+ if version is not None and version.strip() != '':
+ cmd = "hg checkout %s %s" % (verboseflag, sanitized(version))
+ else:
+ cmd = "hg update %s --config ui.merge=internal:fail" % verboseflag
+ value, _, _ = run_shell_command(cmd,
+ cwd=self._path,
+ shell=True,
+ no_filter=True)
+ if value != 0:
+ return False
+ return True
+
+ def get_version(self, spec=None):
+ """
+ :param spec: (optional) token for identifying version. spec can be
+ a whatever is allowed by 'hg log -r', e.g. a tagname, sha-ID,
+ revision-number
+ :returns: the current SHA-ID of the repository. Or if spec is
+ provided, the SHA-ID of a revision specified by some
+ token.
+ """
+ # detect presence only if we need path for cwd in popen
+ if spec is not None:
+ if self.detect_presence():
+ command = 'hg log -r %s' % sanitized(spec)
+ repeated = False
+ output = ''
+ # we repeat the call once after pullin if necessary
+ while output == '':
+ _, output, _ = run_shell_command(command,
+ shell=True,
+ cwd=self._path,
+ us_env=True)
+ if (output.strip() != ''
+ and not output.startswith("abort")
+ or repeated is True):
+
+ matches = [l for l in output.splitlines() if l.startswith('changeset: ')]
+ if len(matches) == 1:
+ return matches[0].split(':')[2]
+ else:
+ sys.stderr.write("Warning: found several candidates for hg spec %s" % spec)
+ break
+ self._do_pull()
+ repeated = True
+ return None
+ else:
+ command = 'hg identify -i %s' % self._path
+ _, output, _ = run_shell_command(command, shell=True, us_env=True)
+ if output is None or output.strip() == '' or output.startswith("abort"):
+ return None
+ # hg adds a '+' to the end if there are uncommited
+ # changes, inconsistent to hg log
+ return output.strip().rstrip('+')
+
+ def get_diff(self, basepath=None):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ command = "hg diff -g %(path)s --repository %(path)s" % {'path': sanitized(rel_path)}
+ _, response, _ = run_shell_command(command, shell=True, cwd=basepath)
+ response = _hg_diff_path_change(response, rel_path)
+ return response
+
+ def get_log(self, relpath=None, limit=None):
+ response = []
+
+ if relpath is None:
+ relpath = ''
+
+ if self.path_exists() and os.path.exists(os.path.join(self._path, relpath)):
+ # Get the log
+ limit_cmd = (("--limit %d" % (int(limit))) if limit else "")
+ HG_COMMIT_FIELDS = ['id', 'author', 'email', 'date', 'message']
+ HG_LOG_FORMAT = '\x1f'.join(['{node|short}', '{author|person}',
+ '{autor|email}', '{date|isodate}',
+ '{desc}']) + '\x1e'
+
+ command = "hg log %s --template '%s' %s" % (sanitized(relpath),
+ HG_LOG_FORMAT,
+ limit_cmd)
+
+ return_code, response_str, stderr = run_shell_command(command, shell=True, cwd=self._path)
+
+ if return_code == 0:
+ # Parse response
+ response = response_str.strip('\n\x1e').split("\x1e")
+ response = [row.strip().split("\x1f") for row in response]
+ response = [dict(zip(HG_COMMIT_FIELDS, row)) for row in response]
+ # Parse dates
+ for entry in response:
+ entry['date'] = dateutil.parser.parse(entry['date'])
+
+ return response
+
+ def get_status(self, basepath=None, untracked=False):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ # protect against shell injection
+ command = "hg status %(path)s --repository %(path)s" % {'path': sanitized(rel_path)}
+ if not untracked:
+ command += " -mard"
+ _, response, _ = run_shell_command(command,
+ shell=True,
+ cwd=basepath)
+ if response is not None:
+ if response.startswith("abort"):
+ raise VcsError("Probable Bug; Could not call %s, cwd=%s" % (command, basepath))
+ if len(response) > 0 and response[-1] != '\n':
+ response += '\n'
+ return response
+
+ def export_repository(self, version, basepath):
+ # execute the hg archive cmd
+ cmd = 'hg archive -t tar -r {0} {1}.tar'.format(version, basepath)
+ result, _, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+ if result:
+ return False
+ try:
+ # gzip the tar file
+ with open(basepath + '.tar', 'rb') as tar_file:
+ gzip_file = gzip.open(basepath + '.tar.gz', 'wb')
+ try:
+ gzip_file.writelines(tar_file)
+ finally:
+ gzip_file.close()
+ finally:
+ # clean up
+ os.remove(basepath + '.tar')
+ return True
+
+ def _do_pull(self):
+ value, _, _ = run_shell_command("hg pull",
+ cwd=self._path,
+ shell=True,
+ no_filter=True)
+ return value == 0
+
+# backwards compat
+HGClient = HgClient
diff --git a/src/vcstools/svn.py b/src/vcstools/svn.py
new file mode 100755
index 0000000..78f3edc
--- /dev/null
+++ b/src/vcstools/svn.py
@@ -0,0 +1,280 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+"""
+svn vcs support.
+"""
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import sys
+
+import tarfile
+
+import dateutil.parser # For parsing date strings
+import xml.dom.minidom # For parsing logfiles
+
+from vcstools.vcs_base import VcsClientBase, VcsError
+from vcstools.common import sanitized, normalized_rel_path, \
+ run_shell_command, ensure_dir_notexists
+
+
+def _get_svn_version():
+ """Looks up svn version by calling svn --version.
+ :raises: VcsError if svn is not installed"""
+ try:
+ # SVN commands produce differently formatted output for french locale
+ value, output, _ = run_shell_command('svn --version',
+ shell=True,
+ us_env=True)
+ if value == 0 and output is not None and len(output.splitlines()) > 0:
+ version = output.splitlines()[0]
+ else:
+ raise VcsError("svn --version returned "
+ + "%s maybe svn is not installed" % value)
+ except VcsError as exc:
+ raise VcsError("Could not determine whether svn is installed: "
+ + str(exc))
+ return version
+
+
+class SvnClient(VcsClientBase):
+
+ def __init__(self, path):
+ """
+ :raises: VcsError if python-svn not detected
+ """
+ VcsClientBase.__init__(self, 'svn', path)
+ # test for svn here, we need it for status
+ _get_svn_version()
+
+ @staticmethod
+ def get_environment_metadata():
+ metadict = {}
+ try:
+ metadict["version"] = _get_svn_version()
+ except:
+ metadict["version"] = "no svn installed"
+ return metadict
+
+ def get_url(self):
+ """
+ :returns: SVN URL of the directory path (output of svn info command),
+ or None if it cannot be determined
+ """
+ if self.detect_presence():
+ #3305: parsing not robust to non-US locales
+ cmd = 'svn info %s' % self._path
+ _, output, _ = run_shell_command(cmd, shell=True)
+ matches = [l for l in output.splitlines() if l.startswith('URL: ')]
+ if matches:
+ return matches[0][5:]
+
+ def detect_presence(self):
+ return self.path_exists() and \
+ os.path.isdir(os.path.join(self.get_path(), '.svn'))
+
+ def checkout(self, url, version='', verbose=False, shallow=False):
+ if url is None or url.strip() == '':
+ raise ValueError('Invalid empty url : "%s"' % url)
+ # Need to check as SVN 1.6.17 writes into directory even if not empty
+ if not ensure_dir_notexists(self.get_path()):
+ self.logger.error("Can't remove %s" % self.get_path())
+ return False
+ if version is not None and version != '':
+ if not version.startswith("-r"):
+ version = "-r%s" % version
+ elif version is None:
+ version = ''
+ cmd = 'svn co %s %s %s' % (sanitized(version),
+ sanitized(url),
+ self._path)
+ value, _, msg = run_shell_command(cmd,
+ shell=True,
+ no_filter=True)
+ if value != 0:
+ if msg:
+ self.logger.error('%s' % msg)
+ return False
+ return True
+
+ def update(self, version=None, verbose=False):
+ if not self.detect_presence():
+ sys.stderr.write("Error: cannot update non-existing directory\n")
+ return False
+ # protect against shell injection
+
+ if version is not None and version != '':
+ if not version.startswith("-r"):
+ version = "-r" + version
+ elif version is None:
+ version = ''
+ cmd = 'svn up %s %s --non-interactive' % (sanitized(version),
+ self._path)
+ value, _, _ = run_shell_command(cmd,
+ shell=True,
+ no_filter=True)
+ if value == 0:
+ return True
+ return False
+
+ def get_version(self, spec=None):
+ """
+ :param spec: (optional) spec can be what 'svn info --help'
+ allows, meaning a revnumber, {date}, HEAD, BASE, PREV, or
+ COMMITTED.
+ :returns: current revision number of the repository. Or if spec
+ provided, the number of a revision specified by some
+ token.
+ """
+ command = 'svn info '
+ if spec is not None:
+ if spec.isdigit():
+ # looking up svn with "-r" takes long, and if spec is
+ # a number, all we get from svn is the same number,
+ # unless we try to look at higher rev numbers (in
+ # which case either get the same number, or an error
+ # if the rev does not exist). So we first do a very
+ # quick svn info, and check revision numbers.
+ currentversion = self.get_version(spec=None)
+ # currentversion is like '-r12345'
+ if currentversion is not None and \
+ int(currentversion[2:]) > int(spec):
+ # so if we know revision exist, just return the
+ # number, avoid the long call to svn server
+ return '-r' + spec
+ if spec.startswith("-r"):
+ command += sanitized(spec)
+ else:
+ command += sanitized('-r%s' % spec)
+ command += " %s" % self._path
+ # #3305: parsing not robust to non-US locales
+ _, output, _ = run_shell_command(command, shell=True, us_env=True)
+ if output is not None:
+ matches = \
+ [l for l in output.splitlines() if l.startswith('Revision: ')]
+ if len(matches) == 1:
+ split_str = matches[0].split()
+ if len(split_str) == 2:
+ return '-r' + split_str[1]
+ return None
+
+ def get_diff(self, basepath=None):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ command = 'svn diff %s' % sanitized(rel_path)
+ _, response, _ = run_shell_command(command,
+ shell=True,
+ cwd=basepath)
+ return response
+
+ def get_log(self, relpath=None, limit=None):
+ response = []
+
+ if relpath is None:
+ relpath = ''
+
+ if self.path_exists() and os.path.exists(os.path.join(self._path, relpath)):
+ # Get the log
+ limit_cmd = (("--limit %d" % (int(limit))) if limit else "")
+ command = "svn log %s --xml %s" % (limit_cmd, sanitized(relpath) if len(relpath) > 0 else '')
+ return_code, xml_response, stderr = run_shell_command(command, shell=True, cwd=self._path)
+
+ # Parse response
+ dom = xml.dom.minidom.parseString(xml_response)
+ log_entries = dom.getElementsByTagName("logentry")
+
+ # Extract the entries
+ for log_entry in log_entries:
+ author_tag = log_entry.getElementsByTagName("author")[0]
+ date_tag = log_entry.getElementsByTagName("date")[0]
+ msg_tags = log_entry.getElementsByTagName("msg")
+
+ log_data = dict()
+ log_data['id'] = log_entry.getAttribute("revision")
+ log_data['author'] = author_tag.firstChild.nodeValue
+ log_data['email'] = None
+ log_data['date'] = dateutil.parser.parse(str(date_tag.firstChild.nodeValue))
+ if len(msg_tags) > 0 and msg_tags[0].firstChild:
+ log_data['message'] = msg_tags[0].firstChild.nodeValue
+ else:
+ log_data['message'] = ''
+
+ response.append(log_data)
+
+ return response
+
+ def get_status(self, basepath=None, untracked=False):
+ response = None
+ if basepath is None:
+ basepath = self._path
+ if self.path_exists():
+ rel_path = normalized_rel_path(self._path, basepath)
+ # protect against shell injection
+ command = 'svn status %s' % sanitized(rel_path)
+ if not untracked:
+ command += " -q"
+ _, response, _ = run_shell_command(command,
+ shell=True,
+ cwd=basepath)
+ if response is not None and \
+ len(response) > 0 and \
+ response[-1] != '\n':
+ response += '\n'
+ return response
+
+ def export_repository(self, version, basepath):
+ # Run the svn export cmd
+ cmd = 'svn export {0} {1}'.format(os.path.join(self._path, version),
+ basepath)
+ result, _, _ = run_shell_command(cmd, shell=True)
+ if result:
+ return False
+ try:
+ # tar gzip the exported repo
+ targzip_file = tarfile.open(basepath + '.tar.gz', 'w:gz')
+ try:
+ targzip_file.add(basepath, '')
+ finally:
+ targzip_file.close()
+ finally:
+ # clean up
+ from shutil import rmtree
+ rmtree(basepath)
+ return True
+
+
+SVNClient = SvnClient
diff --git a/src/vcstools/tar.py b/src/vcstools/tar.py
new file mode 100644
index 0000000..af06272
--- /dev/null
+++ b/src/vcstools/tar.py
@@ -0,0 +1,181 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+"""
+tar vcs support.
+
+The implementation uses the "version" argument to indicate a subfolder
+within a tarfile. Hence one can organize sources by creating one
+tarfile with a folder inside for each version.
+"""
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import tempfile
+import shutil
+import tarfile
+import sys
+import yaml
+from vcstools.vcs_base import VcsClientBase, VcsError
+from vcstools.common import urlretrieve_netrc, ensure_dir_notexists
+
+
+__pychecker__ = 'unusednames=spec'
+
+_METADATA_FILENAME = ".tar"
+
+
+class TarClient(VcsClientBase):
+
+ def __init__(self, path):
+ """
+ @raise VcsError if tar not detected
+ """
+ VcsClientBase.__init__(self, 'tar', path)
+ self.metadata_path = os.path.join(self._path, _METADATA_FILENAME)
+
+ @staticmethod
+ def get_environment_metadata():
+ metadict = {}
+ metadict["version"] = 'tarfile version: %s' % tarfile.__version__
+ return metadict
+
+ def get_url(self):
+ """
+ :returns: TAR URL of the directory path (output of tar info
+ command), or None if it cannot be determined
+ """
+ if self.detect_presence():
+ with open(self.metadata_path, 'r') as metadata_file:
+ metadata = yaml.load(metadata_file.read())
+ if 'url' in metadata:
+ return metadata['url']
+ return None
+
+ def detect_presence(self):
+ return self.path_exists() and os.path.exists(self.metadata_path)
+
+ def checkout(self, url, version='', verbose=False, shallow=False):
+ """
+ untars tar at url to self.path.
+ If version was given, only the subdirectory 'version' of the
+ tar will end up in self.path. Also creates a file next to the
+ checkout named *.tar which is a yaml file listing origin url
+ and version arguments.
+ """
+ if not ensure_dir_notexists(self.get_path()):
+ self.logger.error("Can't remove %s" % self.get_path())
+ return False
+ tempdir = None
+ result = False
+ try:
+ tempdir = tempfile.mkdtemp()
+ if os.path.isfile(url):
+ filename = url
+ else:
+ (filename, _) = urlretrieve_netrc(url)
+ # print "filename", filename
+ temp_tarfile = tarfile.open(filename, 'r:*')
+ members = None # means all members in extractall
+ if version == '' or version is None:
+ self.logger.warn("No tar subdirectory chosen via the 'version' argument for url: %s" % url)
+ else:
+ # getmembers lists all files contained in tar with
+ # relative path
+ subdirs = []
+ members = []
+ for m in temp_tarfile.getmembers():
+ if m.name.startswith(version + '/'):
+ members.append(m)
+ if m.name.split('/')[0] not in subdirs:
+ subdirs.append(m.name.split('/')[0])
+ if not members:
+ raise VcsError("%s is not a subdirectory with contents in members %s" % (version, subdirs))
+ temp_tarfile.extractall(path=tempdir, members=members)
+
+ subdir = os.path.join(tempdir, version)
+ if not os.path.isdir(subdir):
+ raise VcsError("%s is not a subdirectory\n" % subdir)
+
+ try:
+ #os.makedirs(os.path.dirname(self._path))
+ shutil.move(subdir, self._path)
+ except Exception as ex:
+ raise VcsError("%s failed to move %s to %s" % (ex, subdir, self._path))
+ metadata = yaml.dump({'url': url, 'version': version})
+ with open(self.metadata_path, 'w') as mdat:
+ mdat.write(metadata)
+ result = True
+
+ except Exception as exc:
+ self.logger.error("Tarball download unpack failed: %s" % str(exc))
+ finally:
+ if tempdir is not None and os.path.exists(tempdir):
+ shutil.rmtree(tempdir)
+ return result
+
+ def update(self, version='', verbose=False):
+ """
+ Does nothing except returning true if tar exists in same
+ "version" as checked out with vcstools.
+ """
+ if not self.detect_presence():
+ return False
+
+ if version != self.get_version():
+ sys.stderr.write("Tarball Client does not support updating with different version.\n")
+ return False
+
+ return True
+
+ def get_version(self, spec=None):
+
+ if self.detect_presence():
+ with open(self.metadata_path, 'r') as metadata_file:
+ metadata = yaml.load(metadata_file.read())
+ if 'version' in metadata:
+ return metadata['version']
+ return None
+
+ def get_diff(self, basepath=None):
+ return ''
+
+ def get_status(self, basepath=None, untracked=False):
+ return ''
+
+ def export_repository(self, version, basepath):
+ raise VcsError('export repository not implemented for extracted tars')
+
+
+# backwards compatibility
+TARClient = TarClient
diff --git a/src/vcstools/vcs_abstraction.py b/src/vcstools/vcs_abstraction.py
new file mode 100644
index 0000000..746cd96
--- /dev/null
+++ b/src/vcstools/vcs_abstraction.py
@@ -0,0 +1,138 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import warnings
+
+
+_VCS_TYPES = {}
+
+
+def register_vcs(vcs_type, clazz):
+ """
+ :param vcs_type: id, ``str``
+ :param clazz: class extending VcsClientBase
+ """
+ _VCS_TYPES[vcs_type] = clazz
+
+
+def get_registered_vcs_types():
+ """
+ :returns: list of valid key to use as vcs_type
+ """
+ return list(_VCS_TYPES.keys())
+
+
+def get_vcs(vcs_type):
+ """
+ Returns the class interfacing with vcs of given type
+
+ :param vcs_type: id of the tpye, e.g. git, svn, hg, bzr
+ :returns: class extending VcsClientBase
+ :raises: ValueError for unknown vcs_type
+ """
+ vcs_class = _VCS_TYPES.get(vcs_type, None)
+ if not vcs_class:
+ raise ValueError('No Client type registered for vcs type "%s"' % vcs_type)
+ return vcs_class
+
+
+def get_vcs_client(vcs_type, path):
+ """
+ Returns a client with which to interact with the vcs at given path
+
+ :param vcs_type: id of the tpye, e.g. git, svn, hg, bzr
+ :returns: instance of VcsClientBase
+ :raises: ValueError for unknown vcs_type
+ """
+ clientclass = get_vcs(vcs_type)
+ return clientclass(path)
+
+
+class VcsClient(object):
+ """
+ *DEPRECATED* API for interacting with source-controlled paths
+ independent of actual version-control implementation.
+ """
+
+ def __init__(self, vcs_type, path):
+ self._path = path
+ warnings.warn("Class VcsClient is deprecated, use from vcstools" +
+ " import get_vcs_client; get_vcs_client() instead")
+ self.vcs = get_vcs_client(vcs_type, path)
+
+ def path_exists(self):
+ return os.path.exists(self._path)
+
+ def get_path(self):
+ return self._path
+
+ # pass through VCSClientBase API
+ def get_version(self, spec=None):
+ return self.vcs.get_version(spec)
+
+ def checkout(self, url, version='', verbose=False, shallow=False):
+ return self.vcs.checkout(url,
+ version,
+ verbose=verbose,
+ shallow=shallow)
+
+ def url_matches(self, url, url_or_shortcut):
+ return self.vcs.url_matches(url=url, url_or_shortcut=url_or_shortcut)
+
+ def update(self, version='', verbose=False):
+ return self.vcs.update(version, verbose=verbose)
+
+ def detect_presence(self):
+ return self.vcs.detect_presence()
+
+ def get_vcs_type_name(self):
+ return self.vcs.get_vcs_type_name()
+
+ def get_url(self):
+ return self.vcs.get_url()
+
+ def get_diff(self, basepath=None):
+ return self.vcs.get_diff(basepath)
+
+ def get_status(self, basepath=None, untracked=False):
+ return self.vcs.get_status(basepath, untracked)
+
+ def get_log(self, relpath=None, limit=None):
+ return self.vcs.get_log(relpath, limit)
+
+ def export_repository(self, version, basepath):
+ return self.vcs.export_repository(version, basepath)
+
+# backwards compat
+VCSClient = VcsClient
diff --git a/src/vcstools/vcs_base.py b/src/vcstools/vcs_base.py
new file mode 100644
index 0000000..2d5aa5e
--- /dev/null
+++ b/src/vcstools/vcs_base.py
@@ -0,0 +1,244 @@
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+"""
+vcs support library base class.
+"""
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import logging
+
+
+__pychecker__ = 'unusednames=spec,url,version,basepath,untracked'
+
+
+class VcsError(Exception):
+ """To be thrown when an SCM Client faces a situation because of a
+ violated assumption"""
+
+ def __init__(self, value):
+ super(VcsError, self).__init__(value)
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+
+class VcsClientBase(object):
+ """
+ parent class for all vcs clients, provides their public API
+ """
+
+ def __init__(self, vcs_type_name, path):
+ """
+ subclasses may raise VcsError when a dependency is missing
+ """
+ self._path = path
+ if path is None:
+ raise VcsError("Cannot initialize VCSclient without path")
+ self._vcs_type_name = vcs_type_name
+ self.logger = logging.getLogger('vcstools')
+
+ @staticmethod
+ def get_environment_metadata():
+ """
+ For debugging purposes, returns a dict containing information
+ about the environment, like the version of the SCM client, or
+ version of libraries involved.
+ Suggest considering keywords "version", "dependency", "features" first.
+ :returns: a dict containing relevant information
+ :rtype: dict
+ """
+ raise NotImplementedError(
+ "Base class get_environment_metadata method must be overridden")
+
+ def path_exists(self):
+ """
+ helper function
+ """
+ return os.path.exists(self._path)
+
+ def get_path(self):
+ """
+ returns the path this client was configured for
+ """
+ return self._path
+
+ def url_matches(self, url, url_or_shortcut):
+ """
+ client can decide whether the url and the other url are equivalent.
+ Checks string equality by default
+ :param url_or_shortcut: url or shortcut (e.g. bzr launchpad url)
+ :returns: bool if params are equivalent
+ """
+ if url is None or url_or_shortcut is None:
+ return False
+ return url.rstrip('/') == url_or_shortcut.rstrip('/')
+
+ def get_url(self):
+ """
+ :returns: The source control url for the path
+ :rtype: str
+ """
+ raise NotImplementedError(
+ "Base class get_url method must be overridden for client type %s" %
+ self._vcs_type_name)
+
+ def get_version(self, spec=None):
+ """
+ Find an identifier for a the current or a specified
+ revision. Token spec might be a tagname, branchname,
+ version-id, SHA-ID, ... depending on the VCS implementation.
+
+ :param spec: token for identifying repository revision
+ :type spec: str
+ :returns: current revision number of the repository. Or if
+ spec is provided, the respective revision number.
+ :rtype: str
+ """
+ raise NotImplementedError("Base class get_version method must be overridden for client type %s " %
+ self._vcs_type_name)
+
+ def checkout(self, url, spec=None, verbose=False, shallow=False):
+ """
+ Attempts to create a local repository given a remote
+ url. Fails if a target path exists, unless it's an empty directory.
+ If a spec is provided, the local repository
+ will be updated to that revision. It is possible that
+ after a failed call to checkout, a repository still exists,
+ e.g. if an invalid revision spec was given.
+ If shallow is provided, the scm client may checkout less
+ than the full repository history to save time / disk space.
+
+ :param url: where to checkout from
+ :type url: str
+ :param spec: token for identifying repository revision
+ :type spec: str
+ :param shallow: hint to checkout less than a full repository
+ :type shallow: bool
+ :returns: True if successful
+ """
+ raise NotImplementedError("Base class checkout method must be overridden for client type %s " %
+ self._vcs_type_name)
+
+ def update(self, spec=None, verbose=False):
+ """
+ Sets the local copy of the repository to a version matching
+ the spec. Fails when there are uncommited changes.
+ On failures (also e.g. network failure) grants the
+ checked out files are in the same state as before the call.
+
+ :param spec: token for identifying repository revision
+ desired. Token might be a tagname, branchname, version-id,
+ SHA-ID, ... depending on the VCS implementation.
+ :returns: True on success, False else
+ """
+ raise NotImplementedError("Base class update method must be overridden for client type %s " %
+ self._vcs_type_name)
+
+ def detect_presence(self):
+ """For auto detection"""
+ raise NotImplementedError(
+ "Base class detect_presence method must be overridden")
+
+ def get_vcs_type_name(self):
+ """ used when auto detected """
+ return self._vcs_type_name
+
+ def get_diff(self, basepath=None):
+ """
+ :param basepath: diff paths will be relative to this, if any
+ :returns: A string showing local differences
+ :rtype: str
+ """
+ raise NotImplementedError(
+ "Base class get_diff method must be overridden")
+
+ def get_status(self, basepath=None, untracked=False):
+ """
+ Calls scm status command. Output must be terminated by newline
+ unless empty.
+
+ Semantics of untracked are difficult to generalize.
+ In SVN, this would be new files only. In git,
+ hg, bzr, this would be changes that have not been added for
+ commit.
+
+ :param basepath: status path will be relative to this, if any
+ :param untracked: whether to also show changes that would not commit
+ :returns: A string summarizing locally modified files
+ :rtype: str
+ """
+ raise NotImplementedError("Base class get_status method must be overridden for client type %s " %
+ self._vcs_type_name)
+
+ def get_log(self, relpath=None, limit=None):
+ """
+ Calls scm log command.
+
+ This returns a list of dictionaries with the following fields:
+ - id: the commit SHA or revision number
+ - date: the date the commit was made (python datetime)
+ - author: the name of the author of the commit, if available
+ - email: the e-mail address of the author of the commit
+ - message: the commit message, if any
+
+ :param relpath: (optional) restrict logs to events on this
+ resource path (folder or file) relative to the root of the
+ repository. If None (default), this is the root of the
+ repository.
+ :param limit: (optional) the maximum number of log entries
+ that should be retrieved. If None (default), there is no
+ limit.
+ """
+ raise NotImplementedError(
+ "Base class get_log method must be overridden")
+
+ def export_repository(self, version, basepath):
+ """
+ Calls scm equivalent to `svn export`, removing scm meta
+ information and tar gzip'ing the repository at a given version
+ to the given basepath.
+
+ :param version: version of the repository to export. This can
+ be a branch, tag, or path (svn). When specifying the version
+ as a path for svn, the path should be relative to the root of
+ the svn repository, i.e. 'trunk', or 'tags/1.2.3', or './' for
+ the root.
+ :param basepath: this is the path to the tar gzip, excluding
+ the extension which will be .tar.gz
+ :returns: True on success, False otherwise.
+ """
+ raise NotImplementedError("Base class export_repository method must be overridden for client type %s " %
+ self._vcs_type_name)
diff --git a/stdeb.cfg b/stdeb.cfg
new file mode 100644
index 0000000..577b22e
--- /dev/null
+++ b/stdeb.cfg
@@ -0,0 +1,4 @@
+[DEFAULT]
+Depends: subversion, mercurial, git-core, bzr, python-yaml, python-dateutil
+Suite: lucid oneiric precise quantal raring saucy wheezy
+XS-Python-Version: >= 2.6
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/test_base.py b/test/test_base.py
new file mode 100644
index 0000000..9090f0e
--- /dev/null
+++ b/test/test_base.py
@@ -0,0 +1,160 @@
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import unittest
+import tempfile
+import shutil
+from mock import Mock
+
+import vcstools
+from vcstools.vcs_base import VcsClientBase, VcsError
+from vcstools.common import sanitized, normalized_rel_path, \
+ run_shell_command, urlretrieve_netrc, _netrc_open, urlopen_netrc
+
+
+class BaseTest(unittest.TestCase):
+
+ def test_normalized_rel_path(self):
+ self.assertEqual(None, normalized_rel_path(None, None))
+ self.assertEqual('foo', normalized_rel_path(None, 'foo'))
+ self.assertEqual('/foo', normalized_rel_path(None, '/foo'))
+ self.assertEqual('../bar', normalized_rel_path('/bar', '/foo'))
+ self.assertEqual('../bar', normalized_rel_path('/bar', '/foo/baz/..'))
+ self.assertEqual('../bar', normalized_rel_path('/bar/bam/foo/../..', '/foo/baz/..'))
+ self.assertEqual('bar', normalized_rel_path('bar/bam/foo/../..', '/foo/baz/..'))
+
+ def test_sanitized(self):
+ self.assertEqual('', sanitized(None))
+ self.assertEqual('', sanitized(''))
+ self.assertEqual('"foo"', sanitized('foo'))
+ self.assertEqual('"foo"', sanitized('\"foo\"'))
+ self.assertEqual('"foo"', sanitized('"foo"'))
+ self.assertEqual('"foo"', sanitized('" foo"'))
+
+ try:
+ sanitized('bla"; foo"')
+ self.fail("Expected Exception")
+ except VcsError:
+ pass
+ try:
+ sanitized('bla";foo"')
+ self.fail("Expected Exception")
+ except VcsError:
+ pass
+ try:
+ sanitized('bla";foo \"bum')
+ self.fail("Expected Exception")
+ except VcsError:
+ pass
+ try:
+ sanitized('bla";foo;"bam')
+ self.fail("Expected Exception")
+ except VcsError:
+ pass
+ try:
+ sanitized('bla"#;foo;"bam')
+ self.fail("Expected Exception")
+ except VcsError:
+ pass
+
+ def test_shell_command(self):
+ self.assertEqual((0, "", None), run_shell_command("true"))
+ self.assertEqual((1, "", None), run_shell_command("false"))
+ self.assertEqual((0, "foo", None), run_shell_command("echo foo", shell=True))
+ (v, r, e) = run_shell_command("[", shell=True)
+ self.assertFalse(v == 0)
+ self.assertFalse(e is None)
+ self.assertEqual(r, '')
+ (v, r, e) = run_shell_command("echo foo && [", shell=True)
+ self.assertFalse(v == 0)
+ self.assertFalse(e is None)
+ self.assertEqual(r, 'foo')
+ # not a great test on a system where this is default
+ _, env_langs, _ = run_shell_command("/usr/bin/env |grep LANG=", shell=True, us_env=True)
+ self.assertTrue("LANG=en_US.UTF-8" in env_langs.splitlines())
+ try:
+ run_shell_command("two words")
+ self.fail("expected exception")
+ except:
+ pass
+
+ def test_shell_command_verbose(self):
+ # just check no Exception happens due to decoding
+ run_shell_command("echo %s" % (b'\xc3\xa4'.decode('UTF-8')), shell=True, verbose=True)
+ run_shell_command(["echo", b'\xc3\xa4'.decode('UTF-8')], verbose=True)
+
+ def test_netrc_open(self):
+ root_directory = tempfile.mkdtemp()
+ machine = 'foo.org'
+ uri = 'https://%s/bim/bam' % machine
+ netrcname = os.path.join(root_directory, "netrc")
+ mock_build_opener = Mock()
+ mock_build_opener_fun = Mock()
+ mock_build_opener_fun.return_value = mock_build_opener
+ back_build_opener = vcstools.common.build_opener
+ try:
+ vcstools.common.build_opener = mock_build_opener_fun
+ filelike = _netrc_open(uri, netrcname)
+ self.assertFalse(filelike)
+
+ with open(netrcname, 'w') as fhand:
+ fhand.write(
+ 'machine %s login fooname password foopass' % machine)
+ filelike = _netrc_open(uri, netrcname)
+ self.assertTrue(filelike)
+ filelike = _netrc_open('other', netrcname)
+ self.assertFalse(filelike)
+ filelike = _netrc_open(None, netrcname)
+ self.assertFalse(filelike)
+ finally:
+ shutil.rmtree(root_directory)
+ vcstools.common.build_opener = back_build_opener
+
+ def test_urlopen_netrc(self):
+ mockopen = Mock()
+ mock_result = Mock()
+ backopen = vcstools.common.urlopen
+ backget = vcstools.common._netrc_open
+ try:
+ #monkey-patch with mocks
+ vcstools.common.urlopen = mockopen
+ vcstools.common._netrc_open = Mock()
+ vcstools.common._netrc_open.return_value = mock_result
+ ioe = IOError('MockError')
+ mockopen.side_effect = ioe
+ self.assertRaises(IOError, urlopen_netrc, 'foo')
+ ioe.code = 401
+ result = urlopen_netrc('foo')
+ self.assertEqual(mock_result, result)
+ finally:
+ vcstools.common.urlopen = backopen
+ vcstools.common._netrc_open = backget
+
+ def test_urlretrieve_netrc(self):
+ root_directory = tempfile.mkdtemp()
+ examplename = os.path.join(root_directory, "foo")
+ outname = os.path.join(root_directory, "fooout")
+ with open(examplename, "w") as fhand:
+ fhand.write('content')
+ mockget = Mock()
+ mockopen = Mock()
+ mock_fhand = Mock()
+ backopen = vcstools.common.urlopen
+ backget = vcstools.common._netrc_open
+ try:
+ # vcstools.common.urlopen = mockopen
+ # vcstools.common.urlopen.return_value = mock_fhand
+ # mock_fhand.read.return_value = 'content'
+ mockopen.open.return_value
+ vcstools.common._netrc_open = Mock()
+ vcstools.common._netrc_open.return_value = mockget
+ (fname, headers) = urlretrieve_netrc('file://' + examplename)
+ self.assertTrue(fname)
+ self.assertFalse(os.path.exists(outname))
+ (fname, headers) = urlretrieve_netrc('file://' + examplename,
+ outname)
+ self.assertEqual(outname, fname)
+ self.assertTrue(os.path.isfile(outname))
+ finally:
+ vcstools.common.urlopen = backopen
+ vcstools.common._netrc_open = backget
+ shutil.rmtree(root_directory)
diff --git a/test/test_bzr.py b/test/test_bzr.py
new file mode 100644
index 0000000..9e78761
--- /dev/null
+++ b/test/test_bzr.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2009, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import platform
+import os
+import io
+import fnmatch
+import shutil
+import subprocess
+import tempfile
+import unittest
+from vcstools.bzr import BzrClient, _get_bzr_version
+
+
+class BzrClientTestSetups(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+ try:
+ subprocess.check_call(["bzr", "whoami"])
+ except subprocess.CalledProcessError:
+ subprocess.check_call(["bzr", "whoami", '"ros ros at ros.org"'])
+
+ self.root_directory = tempfile.mkdtemp()
+ self.directories = dict(setUp=self.root_directory)
+ self.remote_path = os.path.join(self.root_directory, "remote")
+ os.makedirs(self.remote_path)
+
+ # create a "remote" repo
+ subprocess.check_call(["bzr", "init"], cwd=self.remote_path)
+ subprocess.check_call(["touch", "fixed.txt"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "add", "fixed.txt"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "commit", "-m", "initial"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "tag", "test_tag"], cwd=self.remote_path)
+ self.local_version_init = "1"
+
+ # files to be modified in "local" repo
+ subprocess.check_call(["touch", "modified.txt"], cwd=self.remote_path)
+ subprocess.check_call(["touch", "modified-fs.txt"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "add", "modified.txt", "modified-fs.txt"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "commit", "-m", "initial"], cwd=self.remote_path)
+ self.local_version_second = "2"
+
+ subprocess.check_call(["touch", "deleted.txt"], cwd=self.remote_path)
+ subprocess.check_call(["touch", "deleted-fs.txt"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "add", "deleted.txt", "deleted-fs.txt"], cwd=self.remote_path)
+ subprocess.check_call(["bzr", "commit", "-m", "modified"], cwd=self.remote_path)
+ self.local_version = "3"
+
+ self.local_path = os.path.join(self.root_directory, "local")
+
+ @classmethod
+ def tearDownClass(self):
+ for d in self.directories:
+ shutil.rmtree(self.directories[d])
+
+ def tearDown(self):
+ if os.path.exists(self.local_path):
+ shutil.rmtree(self.local_path)
+
+
+class BzrClientTest(BzrClientTestSetups):
+
+ def test_url_matches_with_shortcut_strings(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.url_matches('test1234', 'test1234'))
+
+ def test_url_matches_with_shortcut_strings_slashes(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.url_matches('test1234/', 'test1234'))
+ self.assertTrue(client.url_matches('test1234', 'test1234/'))
+ self.assertTrue(client.url_matches('test1234/', 'test1234/'))
+
+ def get_launchpad_info(self, url):
+ po = subprocess.Popen(["bzr", "info", url], stdout=subprocess.PIPE)
+ output = po.stdout.read()
+ # it is not great to use the same code for testing as in
+ # production, but relying on fixed bzr info output is just as
+ # bad.
+ for line in output.splitlines():
+ sline = line.decode('UTF-8').strip()
+ for prefix in ['shared repository: ',
+ 'repository branch: ',
+ 'branch root: ']:
+ if sline.startswith(prefix):
+ return sline[len(prefix):]
+ return None
+
+ # this test fails on travis with bzr 2.1.4 and python2.6, but
+ # probably due to the messed up source install of bzr using python2.7
+ if not (platform.python_version().startswith('2.6') and
+ '2.1' in _get_bzr_version()):
+ def test_url_matches_with_shortcut(self):
+ # bzr on launchpad should have shared repository
+ client = BzrClient(self.local_path)
+ url = 'lp:bzr'
+ url2 = self.get_launchpad_info(url)
+ self.assertFalse(url2 is None)
+ self.assertTrue(client.url_matches(url2, url), "%s~=%s" % (url, url2))
+
+ # launchpad on launchpad should be a branch root
+ url = 'lp:launchpad'
+ url2 = self.get_launchpad_info(url)
+ self.assertFalse(url2 is None)
+ self.assertTrue(client.url_matches(url2, url), "%s~=%s" % (url, url2))
+
+ def test_get_url_by_reading(self):
+ client = BzrClient(self.local_path)
+ url = self.remote_path
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_url(), self.remote_path)
+ self.assertEqual(client.get_version(), self.local_version)
+ self.assertEqual(client.get_version(self.local_version_init[0:6]), self.local_version_init)
+ self.assertEqual(client.get_version("test_tag"), self.local_version_init)
+
+ def test_get_url_nonexistant(self):
+ local_path = "/tmp/dummy"
+ client = BzrClient(local_path)
+ self.assertEqual(client.get_url(), None)
+
+ def test_get_type_name(self):
+ local_path = "/tmp/dummy"
+ client = BzrClient(local_path)
+ self.assertEqual(client.get_vcs_type_name(), 'bzr')
+
+ def test_checkout_invalid(self):
+ "makes sure failed checkout results in False, not Exception"
+ url = self.remote_path + "foobar"
+ client = BzrClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.checkout(url))
+
+ def test_checkout_invalid_update(self):
+ "makes sure no exception happens on invalid update"
+ url = self.remote_path
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.checkout(url))
+ new_version = 'foobar'
+ self.assertFalse(client.update(new_version))
+
+ def test_checkout(self):
+ url = self.remote_path
+ client = BzrClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+
+ def test_checkout_dir_exists(self):
+ url = self.remote_path
+ client = BzrClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ os.makedirs(self.local_path)
+ self.assertTrue(client.checkout(url))
+ # non-empty
+ self.assertFalse(client.checkout(url))
+
+ def test_checkout_specific_version_and_update(self):
+ url = self.remote_path
+ version = "1"
+ client = BzrClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_version(), version)
+
+ new_version = '2'
+ self.assertTrue(client.update(new_version))
+ self.assertEqual(client.get_version(), new_version)
+
+ def testDiffClean(self):
+ client = BzrClient(self.remote_path)
+ self.assertEquals('', client.get_diff())
+
+ def testStatusClean(self):
+ client = BzrClient(self.remote_path)
+ self.assertEquals('', client.get_status())
+
+
+class BzrClientLogTest(BzrClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ BzrClientTestSetups.setUpClass()
+ client = BzrClient(self.local_path)
+ client.checkout(self.remote_path)
+
+ def test_get_log_defaults(self):
+ client = BzrClient(self.local_path)
+ client.checkout(self.remote_path)
+ log = client.get_log()
+ self.assertEquals(3, len(log))
+ self.assertEquals('modified', log[0]['message'])
+ for key in ['id', 'author', 'email', 'date', 'message']:
+ self.assertTrue(log[0][key] is not None, key)
+
+ def test_get_log_limit(self):
+ client = BzrClient(self.local_path)
+ client.checkout(self.remote_path)
+ log = client.get_log(limit=1)
+ self.assertEquals(1, len(log))
+ self.assertEquals('modified', log[0]['message'])
+
+ def test_get_log_path(self):
+ client = BzrClient(self.local_path)
+ client.checkout(self.remote_path)
+ log = client.get_log(relpath='fixed.txt')
+ self.assertEquals('initial', log[0]['message'])
+
+
+class BzrDiffStatClientTest(BzrClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ # setup a local repo once for all diff and status test
+ BzrClientTestSetups.setUpClass()
+ url = self.remote_path
+ client = BzrClient(self.local_path)
+ client.checkout(url)
+ # after setting up "local" repo, change files and make some changes
+ subprocess.check_call(["rm", "deleted-fs.txt"], cwd=self.local_path)
+ subprocess.check_call(["bzr", "rm", "deleted.txt"], cwd=self.local_path)
+ f = io.open(os.path.join(self.local_path, "modified.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "modified-fs.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added-fs.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ subprocess.check_call(["bzr", "add", "added.txt"], cwd=self.local_path)
+
+ def tearDown(self):
+ pass
+
+ @classmethod
+ def tearDownClass(self):
+ BzrClientTestSetups.tearDownClass()
+
+ def test_diff(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ # using fnmatch because date and time change (remove when bzr reaches diff --format)
+ diff = client.get_diff()
+ self.assertTrue(diff is not None)
+ self.assertTrue(fnmatch.fnmatch(diff, "=== added file 'added.txt'\n--- ./added.txt\t????-??-?? ??:??:?? +0000\n+++ ./added.txt\t????-??-?? ??:??:?? +0000\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end of file\n\n=== removed file 'deleted-fs.txt'\n=== removed file 'deleted.txt'\n=== modified file 'modified-fs.txt'\n--- ./modified-fs.txt\t????-??-?? ??:??:?? +0000\n+++ ./modified-fs.txt\t????-??-?? ??:??:?? +0000\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end [...]
+
+ def test_diff_relpath(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ # using fnmatch because date and time change (remove when bzr introduces diff --format)
+ diff = client.get_diff(basepath=os.path.dirname(self.local_path))
+ self.assertTrue(diff is not None)
+ self.assertTrue(fnmatch.fnmatch(diff, "=== added file 'added.txt'\n--- local/added.txt\t????-??-?? ??:??:?? +0000\n+++ local/added.txt\t????-??-?? ??:??:?? +0000\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end of file\n\n=== removed file 'deleted-fs.txt'\n=== removed file 'deleted.txt'\n=== modified file 'modified-fs.txt'\n--- local/modified-fs.txt\t????-??-?? ??:??:?? +0000\n+++ local/modified-fs.txt\t????-??-?? ??:??:?? +0000\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No [...]
+
+ def test_status(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('+N ./added.txt\n D ./deleted-fs.txt\n-D ./deleted.txt\n M ./modified-fs.txt\n M ./modified.txt\n', client.get_status())
+
+ def test_status_relpath(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('+N local/added.txt\n D local/deleted-fs.txt\n-D local/deleted.txt\n M local/modified-fs.txt\n M local/modified.txt\n', client.get_status(basepath=os.path.dirname(self.local_path)))
+
+ def test_status_untracked(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('? ./added-fs.txt\n+N ./added.txt\n D ./deleted-fs.txt\n-D ./deleted.txt\n M ./modified-fs.txt\n M ./modified.txt\n', client.get_status(untracked=True))
+
+
+class BzrDiffStatClientTest(BzrClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ # setup a local repo once for all diff and status test
+ BzrClientTestSetups.setUpClass()
+ url = self.remote_path
+ client = BzrClient(self.local_path)
+ client.checkout(url)
+
+ self.basepath_export = os.path.join(self.root_directory, 'export')
+
+ def tearDown(self):
+ pass
+
+ @classmethod
+ def tearDownClass(self):
+ BzrClientTestSetups.tearDownClass()
+
+ def test_export_repository(self):
+ client = BzrClient(self.local_path)
+ self.assertTrue(
+ client.export_repository(self.local_version, self.basepath_export)
+ )
+
+ self.assertTrue(os.path.exists(self.basepath_export + '.tar.gz'))
+ self.assertFalse(os.path.exists(self.basepath_export + '.tar'))
+ self.assertFalse(os.path.exists(self.basepath_export))
diff --git a/test/test_code_format.py b/test/test_code_format.py
new file mode 100644
index 0000000..066fb6b
--- /dev/null
+++ b/test/test_code_format.py
@@ -0,0 +1,29 @@
+from __future__ import print_function
+import os
+from pkg_resources import parse_version, get_distribution
+
+
+
+
+def test_pep8_conformance():
+ """Test source code for PEP8 conformance"""
+
+ try:
+ import pep8
+ except:
+ print("Skipping pep8 Tests because pep8.py not installed.")
+ return
+
+ # Skip test if pep8 is not new enough
+ pep8_version = parse_version(get_distribution('pep8').version)
+ needed_version = parse_version('1.0')
+ if pep8_version < needed_version:
+ print("Skipping pep8 Tests because pep8.py is too old")
+ return
+
+ pep8style = pep8.StyleGuide(max_line_length=120)
+ report = pep8style.options.report
+ report.start()
+ pep8style.input_dir(os.path.join('..', 'vcstools', 'src'))
+ report.stop()
+ assert report.total_errors == 0, "Found '{0}' code style errors (and warnings).".format(report.total_errors)
diff --git a/test/test_git.py b/test/test_git.py
new file mode 100644
index 0000000..1c133a1
--- /dev/null
+++ b/test/test_git.py
@@ -0,0 +1,729 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2009, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import io
+import unittest
+import subprocess
+import tempfile
+import shutil
+import types
+
+from vcstools import GitClient
+from vcstools.vcs_base import VcsError
+
+
+class GitClientTestSetups(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+ self.root_directory = tempfile.mkdtemp()
+ # helpful when setting tearDown to pass
+ self.directories = dict(setUp=self.root_directory)
+ self.remote_path = os.path.join(self.root_directory, "remote")
+ self.local_path = os.path.join(self.root_directory, "ros")
+ os.makedirs(self.remote_path)
+
+ # create a "remote" repo
+ subprocess.check_call("git init", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch fixed.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m initial", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git tag test_tag", shell=True, cwd=self.remote_path)
+ # other branch
+ subprocess.check_call("git branch test_branch", shell=True, cwd=self.remote_path)
+
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.readonly_version_init = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # files to be modified in "local" repo
+ subprocess.check_call("touch modified.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch modified-fs.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m initial", shell=True, cwd=self.remote_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.readonly_version_second = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ subprocess.check_call("touch deleted.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch deleted-fs.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m modified", shell=True, cwd=self.remote_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.readonly_version = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+ subprocess.check_call("git tag last_tag", shell=True, cwd=self.remote_path)
+
+ @classmethod
+ def tearDownClass(self):
+ for d in self.directories:
+ shutil.rmtree(self.directories[d])
+
+ def tearDown(self):
+ if os.path.exists(self.local_path):
+ shutil.rmtree(self.local_path)
+
+
+class GitClientTest(GitClientTestSetups):
+
+ def test_get_url_by_reading(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_url(), self.remote_path)
+ self.assertEqual(client.get_version(), self.readonly_version)
+ self.assertEqual(client.get_version(self.readonly_version_init[0:6]), self.readonly_version_init)
+ self.assertEqual(client.get_version("test_tag"), self.readonly_version_init)
+ # private functions
+ self.assertFalse(client.is_local_branch("test_branch"))
+ self.assertTrue(client.is_remote_branch("test_branch"))
+ self.assertTrue(client.is_tag("test_tag"))
+ self.assertFalse(client.is_remote_branch("test_tag"))
+ self.assertFalse(client.is_tag("test_branch"))
+
+ def test_get_url_nonexistant(self):
+ # local_path = "/tmp/dummy"
+ client = GitClient(self.local_path)
+ self.assertEqual(client.get_url(), None)
+
+ def test_get_type_name(self):
+ # local_path = "/tmp/dummy"
+ client = GitClient(self.local_path)
+ self.assertEqual(client.get_vcs_type_name(), 'git')
+
+ def test_checkout(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_branch(), "master")
+ self.assertEqual(client.get_branch_parent(), "master")
+ #self.assertEqual(client.get_version(), '-r*')
+
+ def test_checkout_dir_exists(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ os.makedirs(self.local_path)
+ self.assertTrue(client.checkout(url))
+ # non-empty
+ self.assertFalse(client.checkout(url))
+
+ def test_checkout_no_unnecessary_updates(self):
+ client = GitClient(self.local_path)
+ client.fetches = 0
+ client.submodules = 0
+ client.fast_forwards = 0
+
+ def ifetch(self):
+ self.fetches += 1
+ return True
+
+ def iff(self, fetch=True, branch_parent=None, verbose=False):
+ self.fast_forwards += 1
+ return True
+
+ def isubm(self, verbose=False):
+ self.submodules += 1
+ return True
+ client._do_fetch = types.MethodType(ifetch, client)
+ client._do_fast_forward = types.MethodType(iff, client)
+ client.update_submodules = types.MethodType(isubm, client)
+ url = self.remote_path
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertEqual(0, client.submodules)
+ self.assertEqual(0, client.fetches)
+ self.assertEqual(0, client.fast_forwards)
+ self.assertTrue(client.update())
+ self.assertEqual(1, client.submodules)
+ self.assertEqual(1, client.fetches)
+ self.assertEqual(1, client.fast_forwards)
+ self.assertTrue(client.update('test_branch'))
+ self.assertEqual(2, client.submodules)
+ self.assertEqual(2, client.fetches)
+ self.assertEqual(1, client.fast_forwards)
+ self.assertTrue(client.update('test_branch'))
+ self.assertEqual(3, client.submodules)
+ self.assertEqual(3, client.fetches)
+ self.assertEqual(2, client.fast_forwards)
+
+ def test_checkout_no_unnecessary_updates_other_branch(self):
+ client = GitClient(self.local_path)
+ client.fetches = 0
+ client.submodules = 0
+ client.fast_forwards = 0
+
+ def ifetch(self):
+ self.fetches += 1
+ return True
+
+ def iff(self, fetch=True, branch_parent=None, verbose=False):
+ self.fast_forwards += 1
+ return True
+
+ def isubm(self, verbose=False):
+ self.submodules += 1
+ return True
+ client._do_fetch = types.MethodType(ifetch, client)
+ client._do_fast_forward = types.MethodType(iff, client)
+ client.update_submodules = types.MethodType(isubm, client)
+ url = self.remote_path
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, 'test_branch'))
+ self.assertEqual(1, client.submodules)
+ self.assertEqual(0, client.fetches)
+ self.assertEqual(0, client.fast_forwards)
+
+ def test_checkout_shallow(self):
+ url = 'file://' + self.remote_path
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, shallow=True))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_branch(), "master")
+ self.assertEqual(client.get_branch_parent(), "master")
+ po = subprocess.Popen("git log --pretty=format:%H", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ log = po.stdout.read().decode('UTF-8').splitlines()
+ # shallow only contains last 2 commits
+ self.assertEqual(2, len(log), log)
+
+ def test_checkout_specific_version_and_update(self):
+ url = self.remote_path
+ version = self.readonly_version
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_version(), version)
+
+ new_version = self.readonly_version_second
+ self.assertTrue(client.update(new_version))
+ self.assertEqual(client.get_version(), new_version)
+
+ def test_checkout_master_branch_and_update(self):
+ # subdir = "checkout_specific_version_test"
+ url = self.remote_path
+ branch = "master"
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, branch))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_branch_parent(), branch)
+
+ self.assertTrue(client.update(branch))
+ self.assertEqual(client.get_branch_parent(), branch)
+
+ def test_checkout_specific_branch_and_update(self):
+ # subdir = "checkout_specific_version_test"
+ url = self.remote_path
+ branch = "test_branch"
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, branch))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertTrue(client.is_local_branch(branch))
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_version(), self.readonly_version_init)
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_branch_parent(), branch)
+
+ self.assertTrue(client.update()) # no arg
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_version(), self.readonly_version_init)
+ self.assertEqual(client.get_branch_parent(), branch)
+
+ self.assertTrue(client.update(branch)) # same branch arg
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_version(), self.readonly_version_init)
+ self.assertEqual(client.get_branch_parent(), branch)
+
+ new_branch = 'master'
+ self.assertTrue(client.update(new_branch))
+ self.assertEqual(client.get_branch(), new_branch)
+ self.assertEqual(client.get_branch_parent(), new_branch)
+
+ def test_checkout_specific_tag_and_update(self):
+ url = self.remote_path
+ tag = "last_tag"
+ client = GitClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, tag))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_branch_parent(), None)
+ tag = "test_tag"
+ self.assertTrue(client.update(tag))
+ self.assertEqual(client.get_branch_parent(), None)
+
+ new_branch = 'master'
+ self.assertTrue(client.update(new_branch))
+ self.assertEqual(client.get_branch_parent(), new_branch)
+ tag = "test_tag"
+ self.assertTrue(client.update(tag))
+
+ def test_fast_forward(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertTrue(client.checkout(url, "master"))
+ subprocess.check_call("git reset --hard test_tag", shell=True, cwd=self.local_path)
+ self.assertTrue(client.update())
+
+ def test_fast_forward_diverged(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertTrue(client.checkout(url, "master"))
+ subprocess.check_call("git reset --hard test_tag", shell=True, cwd=self.local_path)
+ subprocess.check_call("touch diverged.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m diverge", shell=True, cwd=self.local_path)
+ # fail because we have diverged
+ self.assertFalse(client.update('master'))
+
+ def test_fast_forward_simple_ref(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertTrue(client.checkout(url, "master"))
+ subprocess.check_call("git reset --hard test_tag", shell=True, cwd=self.local_path)
+ # replace "refs/head/master" with just "master"
+ subprocess.check_call("git config --replace-all branch.master.merge master", shell=True, cwd=self.local_path)
+
+ self.assertTrue(client.get_branch_parent() is not None)
+
+ def testDiffClean(self):
+ client = GitClient(self.remote_path)
+ self.assertEquals('', client.get_diff())
+
+ def testStatusClean(self):
+ client = GitClient(self.remote_path)
+ self.assertEquals('', client.get_status())
+
+
+class GitClientUpdateTest(GitClientTestSetups):
+
+ def test_update_fetch_all_tags(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertTrue(client.checkout(url, "master"))
+ self.assertEqual(client.get_branch(), "master")
+ self.assertTrue(client.update())
+ p = subprocess.Popen("git tag", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ output = p.communicate()[0].decode('utf-8')
+ self.assertEqual('last_tag\ntest_tag\n', output)
+ subprocess.check_call("git checkout test_tag", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git branch alt_branch", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch alt_file.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m altfile", shell=True, cwd=self.remote_path)
+ # switch to untracked
+ subprocess.check_call("git checkout test_tag", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch new_file.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m newfile", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git tag new_tag", shell=True, cwd=self.remote_path)
+ self.assertTrue(client.update())
+ # test whether client gets the tag
+ p = subprocess.Popen("git tag", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ output = p.communicate()[0].decode('utf-8')
+ self.assertEqual('''\
+last_tag
+new_tag
+test_tag
+''', output)
+ p = subprocess.Popen("git branch -a", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ output = p.communicate()[0].decode('utf-8')
+ self.assertEqual('''\
+* master
+ remotes/origin/HEAD -> origin/master
+ remotes/origin/alt_branch
+ remotes/origin/master
+ remotes/origin/test_branch
+''', output)
+
+
+class GitClientLogTest(GitClientTestSetups):
+
+ def setUp(self):
+ client = GitClient(self.local_path)
+ client.checkout(self.remote_path)
+ # Create some local untracking branch
+ subprocess.check_call("git checkout test_tag -b localbranch", shell=True, cwd=self.local_path)
+
+ self.n_commits = 10
+
+ for i in range(self.n_commits):
+ subprocess.check_call("touch local_%d.txt" % i, shell=True, cwd=self.local_path)
+ subprocess.check_call("git add local_%d.txt" % i, shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m \"local_%d\"" % i, shell=True, cwd=self.local_path)
+
+ def test_get_log_defaults(self):
+ client = GitClient(self.local_path)
+ log = client.get_log()
+ self.assertEquals(self.n_commits + 1, len(log))
+ self.assertEquals('local_%d' % (self.n_commits - 1), log[0]['message'])
+ for key in ['id', 'author', 'email', 'date', 'message']:
+ self.assertTrue(log[0][key] is not None, key)
+
+ def test_get_log_limit(self):
+ client = GitClient(self.local_path)
+ log = client.get_log(limit=1)
+ self.assertEquals(1, len(log))
+ self.assertEquals('local_%d' % (self.n_commits - 1), log[0]['message'])
+
+ def test_get_log_path(self):
+ client = GitClient(self.local_path)
+ for count in range(self.n_commits):
+ log = client.get_log(relpath='local_%d.txt' % count)
+ self.assertEquals(1, len(log))
+
+
+class GitClientDanglingCommitsTest(GitClientTestSetups):
+
+ def setUp(self):
+ client = GitClient(self.local_path)
+ client.checkout(self.remote_path)
+ # Create some local untracking branch
+ subprocess.check_call("git checkout test_tag -b localbranch", shell=True, cwd=self.local_path)
+ subprocess.check_call("touch local.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m my_branch", shell=True, cwd=self.local_path)
+ subprocess.check_call("git tag my_branch_tag", shell=True, cwd=self.local_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ self.untracked_version = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # diverged branch
+ subprocess.check_call("git checkout test_tag -b diverged_branch", shell=True, cwd=self.local_path)
+ subprocess.check_call("touch diverged.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m diverged_branch", shell=True, cwd=self.local_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ self.diverged_branch_version = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # Go detached to create some dangling commits
+ subprocess.check_call("git checkout test_tag", shell=True, cwd=self.local_path)
+ # create a commit only referenced by tag
+ subprocess.check_call("touch tagged.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m no_branch", shell=True, cwd=self.local_path)
+ subprocess.check_call("git tag no_br_tag", shell=True, cwd=self.local_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ self.no_br_tag_version = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # create a dangling commit
+ subprocess.check_call("touch dangling.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m dangling", shell=True, cwd=self.local_path)
+
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ self.dangling_version = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # create a dangling tip on top of dangling commit (to catch related bugs)
+ subprocess.check_call("touch dangling-tip.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m dangling_tip", shell=True, cwd=self.local_path)
+
+ # create and delete branch to cause reflog entry
+ subprocess.check_call("git branch oldbranch", shell=True, cwd=self.local_path)
+ subprocess.check_call("git branch -D oldbranch", shell=True, cwd=self.local_path)
+
+ # go back to master to make head point somewhere else
+ subprocess.check_call("git checkout master", shell=True, cwd=self.local_path)
+
+ def test_is_commit_in_orphaned_subtree(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(client.is_commit_in_orphaned_subtree(self.dangling_version))
+ self.assertFalse(client.is_commit_in_orphaned_subtree(self.no_br_tag_version))
+ self.assertFalse(client.is_commit_in_orphaned_subtree(self.diverged_branch_version))
+
+ def test_protect_dangling(self):
+ client = GitClient(self.local_path)
+ # url = self.remote_path
+ self.assertEqual(client.get_branch(), "master")
+ tag = "no_br_tag"
+ self.assertTrue(client.update(tag))
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ tag = "test_tag"
+ self.assertTrue(client.update(tag))
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ # to dangling commit
+ sha = self.dangling_version
+ self.assertTrue(client.update(sha))
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_version(), self.dangling_version)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ # now HEAD protects the dangling commit, should not be allowed to move off.
+ new_branch = 'master'
+ self.assertFalse(client.update(new_branch))
+
+ def test_detached_to_branch(self):
+ client = GitClient(self.local_path)
+ # url = self.remote_path
+ self.assertEqual(client.get_branch(), "master")
+ tag = "no_br_tag"
+ self.assertTrue(client.update(tag))
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ tag = "test_tag"
+ self.assertTrue(client.update(tag))
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_version(), self.readonly_version_init)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ #update should not change anything
+ self.assertTrue(client.update()) # no arg
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_version(), self.readonly_version_init)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ new_branch = 'master'
+ self.assertTrue(client.update(new_branch))
+ self.assertEqual(client.get_branch(), new_branch)
+ self.assertEqual(client.get_version(), self.readonly_version)
+ self.assertEqual(client.get_branch_parent(), new_branch)
+
+ def test_checkout_untracked_branch_and_update(self):
+ # difference to tracked branches is that branch parent is None, and we may hop outside lineage
+ client = GitClient(self.local_path)
+ url = self.remote_path
+ branch = "localbranch"
+ self.assertEqual(client.get_branch(), "master")
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertTrue(client.is_local_branch(branch))
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertTrue(client.update(branch))
+ self.assertEqual(client.get_version(), self.untracked_version)
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ self.assertTrue(client.update()) # no arg
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_version(), self.untracked_version)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ self.assertTrue(client.update(branch)) # same branch arg
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_version(), self.untracked_version)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ # to master
+ new_branch = 'master'
+ self.assertTrue(client.update(new_branch))
+ self.assertEqual(client.get_branch(), new_branch)
+ self.assertEqual(client.get_version(), self.readonly_version)
+ self.assertEqual(client.get_branch_parent(), new_branch)
+
+ # and back
+ self.assertTrue(client.update(branch)) # same branch arg
+ self.assertEqual(client.get_branch(), branch)
+ self.assertEqual(client.get_version(), self.untracked_version)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ # to dangling commit
+ sha = self.dangling_version
+ self.assertTrue(client.update(sha))
+ self.assertEqual(client.get_branch(), None)
+ self.assertEqual(client.get_version(), self.dangling_version)
+ self.assertEqual(client.get_branch_parent(), None)
+
+ #should not work to protect commits from becoming dangled
+ # to commit outside lineage
+ tag = "test_tag"
+ self.assertFalse(client.update(tag))
+
+ def test_inject_protection(self):
+ client = GitClient(self.local_path)
+ try:
+ client.is_tag('foo"; bar"', fetch=False)
+ self.fail("expected Exception")
+ except VcsError:
+ pass
+ try:
+ client.rev_list_contains('foo"; echo bar"', "foo", fetch=False)
+ self.fail("expected Exception")
+ except VcsError:
+ pass
+ try:
+ client.rev_list_contains('foo', 'foo"; echo bar"', fetch=False)
+ self.fail("expected Exception")
+ except VcsError:
+ pass
+ try:
+ client.get_version('foo"; echo bar"')
+ self.fail("expected Exception")
+ except VcsError:
+ pass
+
+
+class GitClientOverflowTest(GitClientTestSetups):
+ '''Test reproducing an overflow of arguments to git log'''
+
+ def setUp(self):
+ client = GitClient(self.local_path)
+ client.checkout(self.remote_path)
+ subprocess.check_call("git checkout test_tag", shell=True, cwd=self.local_path)
+ subprocess.check_call("echo 0 >> count.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git add count.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git commit -m modified-0", shell=True, cwd=self.local_path)
+ # produce many tags to make git log command fail if all are added
+ for count in range(4000):
+ subprocess.check_call("git tag modified-%s" % count, shell=True, cwd=self.local_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.local_path, stdout=subprocess.PIPE)
+ self.last_version = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ def test_orphaned_overflow(self):
+ client = GitClient(self.local_path)
+ # this failed when passing all ref ids to git log
+ self.assertFalse(client.is_commit_in_orphaned_subtree(self.last_version))
+
+
+class GitDiffStatClientTest(GitClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ GitClientTestSetups.setUpClass()
+
+ client = GitClient(self.local_path)
+ client.checkout(self.remote_path, self.readonly_version)
+ # after setting up "readonly" repo, change files and make some changes
+ subprocess.check_call("rm deleted-fs.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("git rm deleted.txt", shell=True, cwd=self.local_path)
+ f = io.open(os.path.join(self.local_path, "modified.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "modified-fs.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ subprocess.check_call("git add modified.txt", shell=True, cwd=self.local_path)
+ f = io.open(os.path.join(self.local_path, "added-fs.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ subprocess.check_call("git add added.txt", shell=True, cwd=self.local_path)
+
+ def tearDown(self):
+ pass
+
+ def testDiff(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('diff --git ./added.txt ./added.txt\nnew file mode 100644\nindex 0000000..454f6b3\n--- /dev/null\n+++ ./added.txt\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at end of file\ndiff --git ./deleted-fs.txt ./deleted-fs.txt\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git ./deleted.txt ./deleted.txt\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git ./modified-fs.txt ./modified-fs.txt\nindex e69de29..454f6b3 100644\n--- ./modified-fs.txt\ [...]
+
+ def testDiffRelpath(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('diff --git ros/added.txt ros/added.txt\nnew file mode 100644\nindex 0000000..454f6b3\n--- /dev/null\n+++ ros/added.txt\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at end of file\ndiff --git ros/deleted-fs.txt ros/deleted-fs.txt\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git ros/deleted.txt ros/deleted.txt\ndeleted file mode 100644\nindex e69de29..0000000\ndiff --git ros/modified-fs.txt ros/modified-fs.txt\nindex e69de29..454f6b3 100644\n--- [...]
+
+ def testStatus(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('A ./added.txt\n D ./deleted-fs.txt\nD ./deleted.txt\n M ./modified-fs.txt\nM ./modified.txt\n', client.get_status())
+
+ def testStatusRelPath(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('A ros/added.txt\n D ros/deleted-fs.txt\nD ros/deleted.txt\n M ros/modified-fs.txt\nM ros/modified.txt\n', client.get_status(basepath=os.path.dirname(self.local_path)))
+
+ def testStatusUntracked(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('A ./added.txt\n D ./deleted-fs.txt\nD ./deleted.txt\n M ./modified-fs.txt\nM ./modified.txt\n?? ./added-fs.txt\n', client.get_status(untracked=True))
+
+
+class GitExportClientTest(GitClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ GitClientTestSetups.setUpClass()
+
+ client = GitClient(self.local_path)
+ client.checkout(self.remote_path, self.readonly_version)
+
+ self.basepath_export = os.path.join(self.root_directory, 'export')
+
+ def tearDown(self):
+ pass
+
+ def testExportRepository(self):
+ client = GitClient(self.local_path)
+ self.assertTrue(
+ client.export_repository(self.readonly_version,
+ self.basepath_export)
+ )
+
+ self.assertTrue(os.path.exists(self.basepath_export + '.tar.gz'))
+ self.assertFalse(os.path.exists(self.basepath_export + '.tar'))
+ self.assertFalse(os.path.exists(self.basepath_export))
diff --git a/test/test_git_subm.py b/test/test_git_subm.py
new file mode 100644
index 0000000..97ffa83
--- /dev/null
+++ b/test/test_git_subm.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2009, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import unittest
+import subprocess
+import tempfile
+import shutil
+
+from vcstools.git import GitClient
+
+
+class GitClientTestSetups(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+
+ self.root_directory = tempfile.mkdtemp()
+ # helpful when setting tearDown to pass
+ self.directories = dict(setUp=self.root_directory)
+ self.remote_path = os.path.join(self.root_directory, "remote")
+ self.submodule_path = os.path.join(self.root_directory, "submodule")
+ self.subsubmodule_path = os.path.join(self.root_directory, "subsubmodule")
+ self.local_path = os.path.join(self.root_directory, "local")
+ self.sublocal_path = os.path.join(self.local_path, "submodule")
+ self.sublocal2_path = os.path.join(self.local_path, "submodule2")
+ self.subsublocal_path = os.path.join(self.sublocal_path, "subsubmodule")
+ os.makedirs(self.remote_path)
+ os.makedirs(self.submodule_path)
+ os.makedirs(self.subsubmodule_path)
+
+ # create a "remote" repo
+ subprocess.check_call("git init", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch fixed.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git add fixed.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m initial", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git tag test_tag", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git branch test_branch", shell=True, cwd=self.remote_path)
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.version_init = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # create a submodule repo
+ subprocess.check_call("git init", shell=True, cwd=self.submodule_path)
+ subprocess.check_call("touch subfixed.txt", shell=True, cwd=self.submodule_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.submodule_path)
+ subprocess.check_call("git commit -m initial", shell=True, cwd=self.submodule_path)
+ subprocess.check_call("git tag sub_test_tag", shell=True, cwd=self.submodule_path)
+
+ # create a subsubmodule repo
+ subprocess.check_call("git init", shell=True, cwd=self.subsubmodule_path)
+ subprocess.check_call("touch subsubfixed.txt", shell=True, cwd=self.subsubmodule_path)
+ subprocess.check_call("git add *", shell=True, cwd=self.subsubmodule_path)
+ subprocess.check_call("git commit -m initial", shell=True, cwd=self.subsubmodule_path)
+ subprocess.check_call("git tag subsub_test_tag", shell=True, cwd=self.subsubmodule_path)
+
+ # attach subsubmodule to submodule
+ subprocess.check_call("git submodule add %s %s" % (self.subsubmodule_path, "subsubmodule"),
+ shell=True, cwd=self.submodule_path)
+ subprocess.check_call("git submodule init", shell=True, cwd=self.submodule_path)
+ subprocess.check_call("git submodule update", shell=True, cwd=self.submodule_path)
+ subprocess.check_call("git commit -m subsubmodule", shell=True, cwd=self.submodule_path)
+
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.subsubmodule_path, stdout=subprocess.PIPE)
+ self.subsubversion_final = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.submodule_path, stdout=subprocess.PIPE)
+ self.subversion_final = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+ # attach submodule to remote
+ subprocess.check_call("git submodule add %s %s" % (self.submodule_path, "submodule"),
+ shell=True, cwd=self.remote_path)
+ subprocess.check_call("git submodule init", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git submodule update", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m submodule", shell=True, cwd=self.remote_path)
+
+ po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.version_final = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+ subprocess.check_call("git tag last_tag", shell=True, cwd=self.remote_path)
+
+ # attach submodule somewhere else in test_branch
+ subprocess.check_call("git checkout master -b test_branch2", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git submodule add %s %s" % (self.submodule_path, "submodule2"), shell=True, cwd=self.remote_path)
+ subprocess.check_call("git submodule init", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git submodule update", shell=True, cwd=self.remote_path)
+ subprocess.check_call("git commit -m submodule", shell=True, cwd=self.remote_path)
+
+ # go back to master else clients will checkout test_branch
+ subprocess.check_call("git checkout master", shell=True, cwd=self.remote_path)
+
+ @classmethod
+ def tearDownClass(self):
+ for d in self.directories:
+ shutil.rmtree(self.directories[d])
+
+ def tearDown(self):
+ if os.path.exists(self.local_path):
+ shutil.rmtree(self.local_path)
+
+
+class GitClientTest(GitClientTestSetups):
+
+ def test_checkout_master_with_subs(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ subclient = GitClient(self.sublocal_path)
+ subsubclient = GitClient(self.subsublocal_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(self.version_final, client.get_version())
+ self.assertTrue(subclient.path_exists())
+ self.assertTrue(subclient.detect_presence())
+ self.assertEqual(self.subversion_final, subclient.get_version())
+ self.assertTrue(subsubclient.path_exists())
+ self.assertTrue(subsubclient.detect_presence())
+ self.assertEqual(self.subsubversion_final, subsubclient.get_version())
+
+ def test_checkout_branch_with_subs(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ subclient = GitClient(self.sublocal_path)
+ subsubclient = GitClient(self.subsublocal_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version='test_branch'))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(self.version_init, client.get_version())
+ self.assertFalse(subclient.path_exists())
+
+ def test_switch_branches(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ subclient = GitClient(self.sublocal_path)
+ subclient2 = GitClient(self.sublocal2_path)
+ subsubclient = GitClient(self.subsublocal_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(subclient.path_exists())
+ self.assertTrue(subsubclient.path_exists())
+ self.assertFalse(subclient2.path_exists())
+ new_version = "test_branch2"
+ self.assertTrue(client.update(new_version))
+ self.assertTrue(subclient2.path_exists())
+
+ def test_status(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertTrue(client.checkout(url))
+ output = client.get_status()
+ self.assertEqual('', output, output)
+
+ with open(os.path.join(self.local_path, 'fixed.txt'), 'a') as f:
+ f.write('0123456789abcdef')
+ subprocess.check_call("touch new.txt", shell=True, cwd=self.local_path)
+ with open(os.path.join(self.sublocal_path, 'subfixed.txt'), 'a') as f:
+ f.write('abcdef0123456789')
+ subprocess.check_call("touch subnew.txt", shell=True, cwd=self.sublocal_path)
+ with open(os.path.join(self.subsublocal_path, 'subsubfixed.txt'), 'a') as f:
+ f.write('012345cdef')
+ subprocess.check_call("touch subsubnew.txt", shell=True, cwd=self.subsublocal_path)
+
+ output = client.get_status()
+ self.assertEqual(' M ./fixed.txt\n M ./submodule\n M ./subfixed.txt\n M ./subsubmodule\n M ./subsubfixed.txt', output.rstrip())
+
+ output = client.get_status(untracked=True)
+ self.assertEqual(' M ./fixed.txt\n M ./submodule\n?? ./new.txt\n M ./subfixed.txt\n M ./subsubmodule\n?? ./subnew.txt\n M ./subsubfixed.txt\n?? ./subsubnew.txt', output.rstrip())
+
+ output = client.get_status(basepath=os.path.dirname(self.local_path), untracked=True)
+ self.assertEqual(' M local/fixed.txt\n M local/submodule\n?? local/new.txt\n M local/subfixed.txt\n M local/subsubmodule\n?? local/subnew.txt\n M local/subsubfixed.txt\n?? local/subsubnew.txt', output.rstrip())
+
+ def test_diff(self):
+ url = self.remote_path
+ client = GitClient(self.local_path)
+ self.assertTrue(client.checkout(url))
+ output = client.get_diff()
+ self.assertEqual('', output, output)
+
+ with open(os.path.join(self.local_path, 'fixed.txt'), 'a') as f:
+ f.write('0123456789abcdef')
+ subprocess.check_call("touch new.txt", shell=True, cwd=self.local_path)
+ with open(os.path.join(self.sublocal_path, 'subfixed.txt'), 'a') as f:
+ f.write('abcdef0123456789')
+ subprocess.check_call("touch subnew.txt", shell=True, cwd=self.sublocal_path)
+ with open(os.path.join(self.subsublocal_path, 'subsubfixed.txt'), 'a') as f:
+ f.write('012345cdef')
+ subprocess.check_call("touch subsubnew.txt", shell=True, cwd=self.subsublocal_path)
+
+ output = client.get_diff()
+ self.assertEqual(1094, len(output))
+ self.assertTrue('diff --git ./fixed.txt ./fixed.txt\nindex e69de29..454f6b3 100644\n--- ./fixed.txt\n+++ ./fixed.txt\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at end of file' in output)
+ self.assertTrue('diff --git ./submodule/subsubmodule/subsubfixed.txt ./submodule/subsubmodule/subsubfixed.txt\nindex e69de29..1a332dc 100644\n--- ./submodule/subsubmodule/subsubfixed.txt\n+++ ./submodule/subsubmodule/subsubfixed.txt\n@@ -0,0 +1 @@\n+012345cdef\n\\ No newline at end of file' in output)
+
+ output = client.get_diff(basepath=os.path.dirname(self.local_path))
+ self.assertEqual(1174, len(output))
+ self.assertTrue('diff --git local/fixed.txt local/fixed.txt\nindex e69de29..454f6b3 100644\n--- local/fixed.txt\n+++ local/fixed.txt\n@@ -0,0 +1 @@\n+0123456789abcdef\n\ No newline at end of file' in output, output)
+ self.assertTrue('diff --git local/submodule/subsubmodule/subsubfixed.txt local/submodule/subsubmodule/subsubfixed.txt\nindex e69de29..1a332dc 100644\n--- local/submodule/subsubmodule/subsubfixed.txt\n+++ local/submodule/subsubmodule/subsubfixed.txt\n@@ -0,0 +1 @@\n+012345cdef\n\ No newline at end of file' in output, output)
diff --git a/test/test_hg.py b/test/test_hg.py
new file mode 100644
index 0000000..98de8dc
--- /dev/null
+++ b/test/test_hg.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2009, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import io
+import unittest
+import subprocess
+import tempfile
+import shutil
+
+from vcstools.hg import HgClient
+
+
+class HGClientTestSetups(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+
+ self.root_directory = tempfile.mkdtemp()
+ self.directories = dict(setUp=self.root_directory)
+ self.remote_path = os.path.join(self.root_directory, "remote")
+ os.makedirs(self.remote_path)
+
+ # create a "remote" repo
+ subprocess.check_call("hg init", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch fixed.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("hg add fixed.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("hg commit -m initial", shell=True, cwd=self.remote_path)
+
+ po = subprocess.Popen("hg log --template '{node|short}' -l1", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.local_version_init = po.stdout.read().decode('UTF-8').rstrip("'").lstrip("'")
+ # in hg, tagging creates an own changeset, so we need to fetch version before tagging
+ subprocess.check_call("hg tag test_tag", shell=True, cwd=self.remote_path)
+
+ # files to be modified in "local" repo
+ subprocess.check_call("touch modified.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch modified-fs.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("hg add modified.txt modified-fs.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("hg commit -m initial", shell=True, cwd=self.remote_path)
+ po = subprocess.Popen("hg log --template '{node|short}' -l1", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.local_version_second = po.stdout.read().decode('UTF-8').rstrip("'").lstrip("'")
+
+ subprocess.check_call("touch deleted.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("touch deleted-fs.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("hg add deleted.txt deleted-fs.txt", shell=True, cwd=self.remote_path)
+ subprocess.check_call("hg commit -m modified", shell=True, cwd=self.remote_path)
+ po = subprocess.Popen("hg log --template '{node|short}' -l1", shell=True, cwd=self.remote_path, stdout=subprocess.PIPE)
+ self.local_version = po.stdout.read().decode('UTF-8').rstrip("'").lstrip("'")
+
+ self.local_path = os.path.join(self.root_directory, "local")
+ self.local_url = self.remote_path
+
+ @classmethod
+ def tearDownClass(self):
+ for d in self.directories:
+ shutil.rmtree(self.directories[d])
+
+ def tearDown(self):
+ if os.path.exists(self.local_path):
+ shutil.rmtree(self.local_path)
+
+
+class HGClientTest(HGClientTestSetups):
+
+ def test_get_url_by_reading(self):
+ url = self.local_url
+ client = HgClient(self.local_path)
+ client.checkout(url, self.local_version)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_url(), self.local_url)
+ self.assertEqual(client.get_version(), self.local_version)
+ self.assertEqual(client.get_version(self.local_version_init[0:6]), self.local_version_init)
+ self.assertEqual(client.get_version("test_tag"), self.local_version_init)
+
+ def test_get_url_nonexistant(self):
+ local_path = "/tmp/dummy"
+ client = HgClient(local_path)
+ self.assertEqual(client.get_url(), None)
+
+ def test_get_type_name(self):
+ local_path = "/tmp/dummy"
+ client = HgClient(local_path)
+ self.assertEqual(client.get_vcs_type_name(), 'hg')
+
+ def test_checkout(self):
+ url = self.local_url
+ client = HgClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_version(), self.local_version)
+
+ def test_checkout_dir_exists(self):
+ url = self.remote_path
+ client = HgClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ os.makedirs(self.local_path)
+ self.assertTrue(client.checkout(url))
+ # non-empty
+ self.assertFalse(client.checkout(url))
+
+ def test_checkout_emptystringversion(self):
+ # special test to check that version '' means the same as None
+ url = self.local_url
+ client = HgClient(self.local_path)
+ self.assertTrue(client.checkout(url, ''))
+ self.assertEqual(client.get_version(), self.local_version)
+
+ # test for #3497
+ def test_checkout_into_subdir_without_existing_parent(self):
+ local_path = os.path.join(self.local_path, "nonexistant_subdir")
+ url = self.local_url
+ client = HgClient(local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), local_path)
+ self.assertEqual(client.get_url(), url)
+
+ def test_checkout_specific_version_and_update(self):
+ url = self.local_url
+ version = self.local_version
+ client = HgClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertEqual(client.get_version(), version)
+
+ new_version = self.local_version_second
+ self.assertTrue(client.update(new_version))
+ self.assertEqual(client.get_version(), new_version)
+
+ self.assertTrue(client.update())
+ self.assertEqual(client.get_version(), self.local_version)
+
+ self.assertTrue(client.update(new_version))
+ self.assertEqual(client.get_version(), new_version)
+
+ self.assertTrue(client.update(''))
+ self.assertEqual(client.get_version(), self.local_version)
+
+ def testDiffClean(self):
+ client = HgClient(self.remote_path)
+ self.assertEquals('', client.get_diff())
+
+ def testStatusClean(self):
+ client = HgClient(self.remote_path)
+ self.assertEquals('', client.get_status())
+
+
+class HGClientLogTest(HGClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ HGClientTestSetups.setUpClass()
+ client = HgClient(self.local_path)
+ client.checkout(self.local_url)
+
+ def test_get_log_defaults(self):
+ client = HgClient(self.local_path)
+ client.checkout(self.local_url)
+ log = client.get_log()
+ self.assertEquals(4, len(log))
+ self.assertEquals('modified', log[0]['message'])
+ for key in ['id', 'author', 'email', 'date', 'message']:
+ self.assertTrue(log[0][key] is not None, key)
+
+ def test_get_log_limit(self):
+ client = HgClient(self.local_path)
+ client.checkout(self.local_url)
+ log = client.get_log(limit=1)
+ self.assertEquals(1, len(log))
+ self.assertEquals('modified', log[0]['message'])
+
+ def test_get_log_path(self):
+ client = HgClient(self.local_path)
+ client.checkout(self.local_url)
+ log = client.get_log(relpath='fixed.txt')
+ self.assertEquals('initial', log[0]['message'])
+
+
+class HGDiffStatClientTest(HGClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ HGClientTestSetups.setUpClass()
+ url = self.local_url
+ client = HgClient(self.local_path)
+ client.checkout(url)
+ # after setting up "local" repo, change files and make some changes
+ subprocess.check_call("rm deleted-fs.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("hg rm deleted.txt", shell=True, cwd=self.local_path)
+ f = io.open(os.path.join(self.local_path, "modified.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "modified-fs.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added-fs.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ subprocess.check_call("hg add added.txt", shell=True, cwd=self.local_path)
+
+ def tearDown(self):
+ pass
+
+ def test_diff(self):
+
+ client = HgClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('diff --git ./added.txt ./added.txt\nnew file mode 100644\n--- /dev/null\n+++ ./added.txt\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end of file\ndiff --git ./deleted.txt ./deleted.txt\ndeleted file mode 100644\ndiff --git ./modified-fs.txt ./modified-fs.txt\n--- ./modified-fs.txt\n+++ ./modified-fs.txt\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end of file\ndiff --git ./modified.txt ./modified.txt\n--- ./modified.txt\n+++ ./modified.txt\n@ [...]
+
+ def test_diff_relpath(self):
+
+ client = HgClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+
+ self.assertEquals('diff --git local/added.txt local/added.txt\nnew file mode 100644\n--- /dev/null\n+++ local/added.txt\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end of file\ndiff --git local/deleted.txt local/deleted.txt\ndeleted file mode 100644\ndiff --git local/modified-fs.txt local/modified-fs.txt\n--- local/modified-fs.txt\n+++ local/modified-fs.txt\n@@ -0,0 +1,1 @@\n+0123456789abcdef\n\\ No newline at end of file\ndiff --git local/modified.txt local/modified.tx [...]
+
+ def test_get_version_modified(self):
+ client = HgClient(self.local_path)
+ self.assertFalse(client.get_version().endswith('+'))
+
+ def test_status(self):
+ client = HgClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('M modified-fs.txt\nM modified.txt\nA added.txt\nR deleted.txt\n! deleted-fs.txt\n', client.get_status())
+
+ def test_status_relpath(self):
+ client = HgClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('M local/modified-fs.txt\nM local/modified.txt\nA local/added.txt\nR local/deleted.txt\n! local/deleted-fs.txt\n', client.get_status(basepath=os.path.dirname(self.local_path)))
+
+ def testStatusUntracked(self):
+ client = HgClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEquals('M modified-fs.txt\nM modified.txt\nA added.txt\nR deleted.txt\n! deleted-fs.txt\n? added-fs.txt\n', client.get_status(untracked=True))
+
+ def test_hg_diff_path_change_None(self):
+ from vcstools.hg import _hg_diff_path_change
+ self.assertEqual(_hg_diff_path_change(None, '/tmp/dummy'), None)
+
+
+class HGExportRepositoryClientTest(HGClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ HGClientTestSetups.setUpClass()
+ url = self.local_url
+ client = HgClient(self.local_path)
+ client.checkout(url)
+
+ self.basepath_export = os.path.join(self.root_directory, 'export')
+
+ def tearDown(self):
+ pass
+
+ def test_export_repository(self):
+ client = HgClient(self.local_path)
+ self.assertTrue(
+ client.export_repository(self.local_version, self.basepath_export)
+ )
+
+ self.assertTrue(os.path.exists(self.basepath_export + '.tar.gz'))
+ self.assertFalse(os.path.exists(self.basepath_export + '.tar'))
+ self.assertFalse(os.path.exists(self.basepath_export))
diff --git a/test/test_svn.py b/test/test_svn.py
new file mode 100644
index 0000000..a2be657
--- /dev/null
+++ b/test/test_svn.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2009, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+import io
+import unittest
+import subprocess
+import tempfile
+import shutil
+import re
+from vcstools.svn import SvnClient
+
+
+class SvnClientTestSetups(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+ self.root_directory = tempfile.mkdtemp()
+ self.directories = dict(setUp=self.root_directory)
+ self.remote_path = os.path.join(self.root_directory, "remote")
+ self.init_path = os.path.join(self.root_directory, "init")
+
+ # create a "remote" repo
+ subprocess.check_call("svnadmin create %s" % self.remote_path, shell=True, cwd=self.root_directory)
+ self.local_root_url = "file://localhost" + self.remote_path
+ self.local_url = self.local_root_url + "/trunk"
+
+ # create an "init" repo to populate remote repo
+ subprocess.check_call("svn checkout %s %s" % (self.local_root_url, self.init_path), shell=True, cwd=self.root_directory)
+
+ for cmd in [
+ "mkdir trunk",
+ "mkdir branches",
+ "mkdir tags",
+ "svn add trunk branches tags",
+ "touch trunk/fixed.txt",
+ "svn add trunk/fixed.txt",
+ "svn commit -m initial"]:
+ subprocess.check_call(cmd, shell=True, cwd=self.init_path)
+
+ self.local_version_init = "-r1"
+
+ # files to be modified in "local" repo
+ for cmd in [
+ "touch trunk/modified.txt",
+ "touch trunk/modified-fs.txt",
+ "svn add trunk/modified.txt trunk/modified-fs.txt",
+ "svn commit -m initial"]:
+ subprocess.check_call(cmd, shell=True, cwd=self.init_path)
+
+ self.local_version_second = "-r2"
+ for cmd in [
+ "touch trunk/deleted.txt",
+ "touch trunk/deleted-fs.txt",
+ "svn add trunk/deleted.txt trunk/deleted-fs.txt",
+ "svn commit -m modified"]:
+ subprocess.check_call(cmd, shell=True, cwd=self.init_path)
+
+ self.local_path = os.path.join(self.root_directory, "local")
+
+ @classmethod
+ def tearDownClass(self):
+ for d in self.directories:
+ shutil.rmtree(self.directories[d])
+
+ def tearDown(self):
+ if os.path.exists(self.local_path):
+ shutil.rmtree(self.local_path)
+
+
+class SvnClientTest(SvnClientTestSetups):
+
+ def test_get_url_by_reading(self):
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(self.local_url, client.get_url())
+ #self.assertEqual(client.get_version(), self.local_version)
+ self.assertEqual(client.get_version("PREV"), "-r2")
+ self.assertEqual(client.get_version("2"), "-r2")
+ self.assertEqual(client.get_version("-r2"), "-r2")
+ # test invalid cient and repo without url
+ client = SvnClient(os.path.join(self.remote_path, 'foo'))
+ self.assertEqual(None, client.get_url())
+
+ def test_get_type_name(self):
+ local_path = "/tmp/dummy"
+ client = SvnClient(local_path)
+ self.assertEqual(client.get_vcs_type_name(), 'svn')
+
+ def test_get_url_nonexistant(self):
+ local_path = "/tmp/dummy"
+ client = SvnClient(local_path)
+ self.assertEqual(client.get_url(), None)
+
+ def test_checkout(self):
+ url = self.local_url
+ client = SvnClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+
+ def test_checkout_dir_exists(self):
+ url = self.local_url
+ client = SvnClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ os.makedirs(self.local_path)
+ self.assertTrue(client.checkout(url))
+ # non-empty
+ self.assertFalse(client.checkout(url))
+
+ def test_checkout_emptyversion(self):
+ url = self.local_url
+ client = SvnClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version=''))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), self.local_path)
+ self.assertEqual(client.get_url(), url)
+ self.assertTrue(client.update(None))
+ self.assertTrue(client.update(""))
+
+ def test_checkout_specific_version_and_update_short(self):
+ "using just a number as version"
+ url = self.local_url
+ version = "3"
+ client = SvnClient(self.local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_version(), "-r3")
+ new_version = '2'
+ self.assertTrue(client.update(new_version))
+ self.assertEqual(client.get_version(), "-r2")
+
+ def testDiffClean(self):
+ client = SvnClient(self.remote_path)
+ self.assertEquals('', client.get_diff())
+
+ def testStatusClean(self):
+ client = SvnClient(self.remote_path)
+ self.assertEquals('', client.get_status())
+
+
+class SvnClientLogTest(SvnClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ SvnClientTestSetups.setUpClass()
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+
+ def test_get_log_defaults(self):
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+ log = client.get_log()
+ self.assertEquals(3, len(log))
+ self.assertEquals('modified', log[0]['message'])
+ for key in ['id', 'author', 'date', 'message']:
+ self.assertTrue(log[0][key] is not None, key)
+ # svn logs don't have email, but key should be in dict
+ self.assertTrue(log[0]['email'] is None)
+
+ def test_get_log_limit(self):
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+ log = client.get_log(limit=1)
+ self.assertEquals(1, len(log))
+ self.assertEquals('modified', log[0]['message'])
+
+ def test_get_log_path(self):
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+ log = client.get_log(relpath='fixed.txt')
+ self.assertEquals('initial', log[0]['message'])
+
+
+class SvnDiffStatClientTest(SvnClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ SvnClientTestSetups.setUpClass()
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+ # after setting up "local" repo, change files and make some changes
+ subprocess.check_call("rm deleted-fs.txt", shell=True, cwd=self.local_path)
+ subprocess.check_call("svn rm deleted.txt", shell=True, cwd=self.local_path)
+ f = io.open(os.path.join(self.local_path, "modified.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "modified-fs.txt"), 'a')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added-fs.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ f = io.open(os.path.join(self.local_path, "added.txt"), 'w')
+ f.write('0123456789abcdef')
+ f.close()
+ subprocess.check_call("svn add added.txt", shell=True, cwd=self.local_path)
+
+ def tearDown(self):
+ pass
+
+ def assertStatusListEqual(self, listexpect, listactual):
+ """helper fun to check scm status output while discarding file ordering differences"""
+ lines_expect = listexpect.splitlines()
+ lines_actual = listactual.splitlines()
+ for line in lines_expect:
+ self.assertTrue(line in lines_actual, 'Missing entry %s in output %s' % (line, listactual))
+ for line in lines_actual:
+ self.assertTrue(line in lines_expect, 'Superflous entry %s in output %s' % (line, listactual))
+
+ def assertEqualDiffs(self, expected, actual):
+ "True if actual is similar enough to expected, minus svn properties"
+
+ def filter_block(block):
+ """removes property information that varies between systems, not relevant fo runit test"""
+ newblock = []
+ for line in block.splitlines():
+ if re.search("[=+-\\@ ].*", line) == None:
+ break
+ else:
+ # new svn versions use different labels for added
+ # files (working copy) vs (revision x)
+ fixedline = re.sub('\(revision [0-9]+\)', '(working copy)', line)
+ newblock.append(fixedline)
+ return "\n".join(newblock)
+
+ filtered_actual_blocks = []
+ # A block starts with \nIndex, and the actual diff goes up to the first line starting with [a-zA-Z], e.g. "Properties changed:"
+ for block in actual.split("\nIndex: "):
+ if filtered_actual_blocks != []:
+ # restore "Index: " removed by split()
+ block = "Index: " + block
+ block = filter_block(block)
+ filtered_actual_blocks.append(block)
+ expected_blocks = []
+ for block in expected.split("\nIndex: "):
+ if expected_blocks != []:
+ block = "Index: " + block
+ block = filter_block(block)
+ expected_blocks.append(block)
+ filtered = "\n".join(filtered_actual_blocks)
+ self.assertEquals(set(expected_blocks), set(filtered_actual_blocks))
+
+ def test_diff(self):
+ client = SvnClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+
+ self.assertEqualDiffs('Index: added.txt\n===================================================================\n--- added.txt\t(revision 0)\n+++ added.txt\t(revision 0)\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at end of file\nIndex: modified-fs.txt\n===================================================================\n--- modified-fs.txt\t(revision 3)\n+++ modified-fs.txt\t(working copy)\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at end of file\nIndex: modified.txt\n== [...]
+ client.get_diff().rstrip())
+
+ def test_diff_relpath(self):
+ client = SvnClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+
+ self.assertEqualDiffs('Index: local/added.txt\n===================================================================\n--- local/added.txt\t(revision 0)\n+++ local/added.txt\t(revision 0)\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at end of file\nIndex: local/modified-fs.txt\n===================================================================\n--- local/modified-fs.txt\t(revision 3)\n+++ local/modified-fs.txt\t(working copy)\n@@ -0,0 +1 @@\n+0123456789abcdef\n\\ No newline at [...]
+
+ def test_status(self):
+ client = SvnClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertStatusListEqual('A added.txt\nD deleted.txt\nM modified-fs.txt\n! deleted-fs.txt\nM modified.txt\n', client.get_status())
+
+ def test_status_relpath(self):
+ client = SvnClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertStatusListEqual('A local/added.txt\nD local/deleted.txt\nM local/modified-fs.txt\n! local/deleted-fs.txt\nM local/modified.txt\n', client.get_status(basepath=os.path.dirname(self.local_path)))
+
+ def test_status_untracked(self):
+ client = SvnClient(self.local_path)
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertStatusListEqual('? added-fs.txt\nA added.txt\nD deleted.txt\nM modified-fs.txt\n! deleted-fs.txt\nM modified.txt\n', client.get_status(untracked=True))
+
+
+class SvnExportRepositoryClientTest(SvnClientTestSetups):
+
+ @classmethod
+ def setUpClass(self):
+ SvnClientTestSetups.setUpClass()
+ client = SvnClient(self.local_path)
+ client.checkout(self.local_url)
+
+ self.basepath_export = os.path.join(self.root_directory, 'export')
+
+ def tearDown(self):
+ pass
+
+ def test_export_repository(self):
+ client = SvnClient(self.local_path)
+ self.assertTrue(
+ client.export_repository('',
+ self.basepath_export)
+ )
+
+ self.assertTrue(os.path.exists(self.basepath_export + '.tar.gz'))
+ self.assertFalse(os.path.exists(self.basepath_export + '.tar'))
+ self.assertFalse(os.path.exists(self.basepath_export))
diff --git a/test/test_tar.py b/test/test_tar.py
new file mode 100644
index 0000000..f77b9a0
--- /dev/null
+++ b/test/test_tar.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2011, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, print_function, unicode_literals
+import os
+import unittest
+import tempfile
+import shutil
+import subprocess
+
+from vcstools.tar import TarClient
+
+
+class TarClientTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+ self.remote_url = "https://code.ros.org/svn/release/download/stacks/exploration/exploration-0.3.0/exploration-0.3.0.tar.bz2"
+ self.package_version = "exploration-0.3.0"
+
+ def setUp(self):
+ self.directories = {}
+
+ def tearDown(self):
+ for d in self.directories:
+ self.assertTrue(os.path.exists(self.directories[d]))
+ shutil.rmtree(self.directories[d])
+ self.assertFalse(os.path.exists(self.directories[d]))
+
+ def test_get_url_by_reading(self):
+ directory = tempfile.mkdtemp()
+ self.directories['local'] = directory
+
+ local_path = os.path.join(directory, "local")
+
+ client = TarClient(local_path)
+ self.assertTrue(client.checkout(self.remote_url, self.package_version))
+
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_url(), self.remote_url)
+ #self.assertEqual(client.get_version(), self.package_version)
+
+ def test_get_url_nonexistant(self):
+ local_path = "/tmp/dummy"
+ client = TarClient(local_path)
+ self.assertEqual(client.get_url(), None)
+
+ def test_get_type_name(self):
+ local_path = "/tmp/dummy"
+ client = TarClient(local_path)
+ self.assertEqual(client.get_vcs_type_name(), 'tar')
+
+ def test_checkout(self):
+ # checks out all subdirs
+ directory = tempfile.mkdtemp()
+ self.directories["checkout_test"] = directory
+ local_path = os.path.join(directory, "exploration")
+ client = TarClient(local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(self.remote_url))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), local_path)
+ self.assertEqual(client.get_url(), self.remote_url)
+ # make sure the tarball subdirectory was promoted correctly.
+ self.assertTrue(os.path.exists(os.path.join(local_path,
+ self.package_version,
+ 'stack.xml')))
+
+ def test_checkout_dir_exists(self):
+ directory = tempfile.mkdtemp()
+ self.directories["checkout_test"] = directory
+ local_path = os.path.join(directory, "exploration")
+ client = TarClient(local_path)
+ self.assertFalse(client.path_exists())
+ os.makedirs(local_path)
+ self.assertTrue(client.checkout(self.remote_url))
+ # non-empty
+ self.assertFalse(client.checkout(self.remote_url))
+
+ def test_checkout_version(self):
+ directory = tempfile.mkdtemp()
+ self.directories["checkout_test"] = directory
+ local_path = os.path.join(directory, "exploration")
+ client = TarClient(local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(self.remote_url,
+ version=self.package_version))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), local_path)
+ self.assertEqual(client.get_url(), self.remote_url)
+ # make sure the tarball subdirectory was promoted correctly.
+ self.assertTrue(os.path.exists(os.path.join(local_path, 'stack.xml')))
+
+
+class TarClientTestLocal(unittest.TestCase):
+
+ def setUp(self):
+ self.root_directory = tempfile.mkdtemp()
+ # helpful when setting tearDown to pass
+ self.directories = dict(setUp=self.root_directory)
+ self.version_path0 = os.path.join(self.root_directory, "version")
+ self.version_path1 = os.path.join(self.root_directory, "version1")
+ self.version_path2 = os.path.join(self.root_directory, "version1.0")
+
+ os.makedirs(self.version_path0)
+ os.makedirs(self.version_path1)
+ os.makedirs(self.version_path2)
+
+ subprocess.check_call("touch stack0.xml", shell=True, cwd=self.version_path0)
+ subprocess.check_call("touch stack.xml", shell=True, cwd=self.version_path1)
+ subprocess.check_call("touch stack1.xml", shell=True, cwd=self.version_path2)
+ subprocess.check_call("touch version1.txt", shell=True, cwd=self.root_directory)
+
+ self.tar_url = os.path.join(self.root_directory, "origin.tar")
+ self.tar_url_compressed = os.path.join(self.root_directory,
+ "origin_compressed.tar.bz2")
+
+ subprocess.check_call("tar -cf %s %s" % (self.tar_url, " ".join(["version",
+ "version1",
+ "version1.txt",
+ "version1.0"])),
+ shell=True,
+ cwd=self.root_directory)
+ subprocess.check_call("tar -cjf %s %s" % (self.tar_url_compressed, " ".join(["version",
+ "version1",
+ "version1.txt",
+ "version1.0"])),
+ shell=True,
+ cwd=self.root_directory)
+
+ def tearDown(self):
+ for d in self.directories:
+ self.assertTrue(os.path.exists(self.directories[d]))
+ shutil.rmtree(self.directories[d])
+ self.assertFalse(os.path.exists(self.directories[d]))
+
+ def test_checkout_version_local(self):
+ directory = tempfile.mkdtemp()
+ self.directories["checkout_test"] = directory
+ local_path = os.path.join(directory, "version1")
+ url = self.tar_url
+ client = TarClient(local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version='version1'))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), local_path)
+ self.assertEqual(client.get_url(), url)
+ # make sure the tarball subdirectory was promoted correctly.
+ self.assertTrue(os.path.exists(os.path.join(local_path, 'stack.xml')))
+
+ def test_checkout_version_compressed_local(self):
+ directory = tempfile.mkdtemp()
+ self.directories["checkout_test"] = directory
+ local_path = os.path.join(directory, "version1")
+ url = self.tar_url_compressed
+ client = TarClient(local_path)
+ self.assertFalse(client.path_exists())
+ self.assertFalse(client.detect_presence())
+ self.assertFalse(client.detect_presence())
+ self.assertTrue(client.checkout(url, version='version1'))
+ self.assertTrue(client.path_exists())
+ self.assertTrue(client.detect_presence())
+ self.assertEqual(client.get_path(), local_path)
+ self.assertEqual(client.get_url(), url)
+ # make sure the tarball subdirectory was promoted correctly.
+ self.assertTrue(os.path.exists(os.path.join(local_path, 'stack.xml')))
diff --git a/test/test_vcs_abstraction.py b/test/test_vcs_abstraction.py
new file mode 100644
index 0000000..68cfd26
--- /dev/null
+++ b/test/test_vcs_abstraction.py
@@ -0,0 +1,50 @@
+from __future__ import absolute_import, print_function, unicode_literals
+import unittest
+from mock import Mock
+
+import vcstools.vcs_abstraction
+from vcstools.vcs_abstraction import register_vcs, get_registered_vcs_types, \
+ get_vcs
+
+from vcstools import get_vcs_client
+
+
+class TestVcsAbstraction(unittest.TestCase):
+
+ def test_register_vcs(self):
+ try:
+ backup = vcstools.vcs_abstraction._VCS_TYPES
+ vcstools.vcs_abstraction._VCS_TYPES = {}
+ self.assertEqual([], get_registered_vcs_types())
+ mock_class = Mock()
+ register_vcs('foo', mock_class)
+ self.assertEqual(['foo'], get_registered_vcs_types())
+ finally:
+ vcstools.vcs_abstraction._VCS_TYPES = backup
+
+ def test_get_vcs(self):
+ try:
+ backup = vcstools.vcs_abstraction._VCS_TYPES
+ vcstools.vcs_abstraction._VCS_TYPES = {}
+ self.assertEqual([], get_registered_vcs_types())
+ mock_class = Mock()
+ register_vcs('foo', mock_class)
+ self.assertEqual(mock_class, get_vcs('foo'))
+ self.assertRaises(ValueError, get_vcs, 'bar')
+ finally:
+ vcstools.vcs_abstraction._VCS_TYPES = backup
+
+ def test_get_vcs_client(self):
+ try:
+ backup = vcstools.vcs_abstraction._VCS_TYPES
+ vcstools.vcs_abstraction._VCS_TYPES = {}
+ self.assertEqual([], get_registered_vcs_types())
+ mock_class = Mock()
+ mock_instance = Mock()
+ # mock __init__ constructor
+ mock_class.return_value = mock_instance
+ register_vcs('foo', mock_class)
+ self.assertEqual(mock_instance, get_vcs_client('foo', 'foopath'))
+ self.assertRaises(ValueError, get_vcs_client, 'bar', 'barpath')
+ finally:
+ vcstools.vcs_abstraction._VCS_TYPES = backup
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/ros/ros-vcstools.git
More information about the debian-science-commits
mailing list