[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