[ros-vcstools] 01/03: New upstream version 0.1.39

Jochen Sprickerhof jspricke at moszumanska.debian.org
Sun Sep 18 07:34:40 UTC 2016


This is an automated email from the git hooks/post-receive script.

jspricke pushed a commit to annotated tag debian/0.1.39-1
in repository ros-vcstools.

commit 00247e9f5fd7ab0b66fb83e4753e614990e161aa
Author: Jochen Sprickerhof <git at jochen.sprickerhof.de>
Date:   Sun Sep 18 09:29:00 2016 +0200

    New upstream version 0.1.39
---
 .gitignore                      |   1 +
 .travis.yml                     |   4 +-
 README.rst                      |  24 +-
 doc/changelog.rst               |   7 +
 setup.py                        |   2 +
 src/vcstools/__version__.py     |   2 +-
 src/vcstools/bzr.py             |  12 +
 src/vcstools/git.py             |  59 +++--
 src/vcstools/git_archive_all.py | 549 ++++++++++++++++++++++++++++++++++++++++
 src/vcstools/hg.py              |   8 +
 src/vcstools/svn.py             |  11 +
 src/vcstools/tar.py             |   7 +-
 src/vcstools/vcs_base.py        |   9 +
 stdeb.cfg                       |   2 +-
 test/test_bzr.py                |  17 ++
 test/test_code_format.py        |   1 +
 test/test_git.py                |  22 ++
 test/test_git_subm.py           | 297 ++++++++++++++++++++--
 test/test_hg.py                 |  19 ++
 test/test_svn.py                |  18 ++
 20 files changed, 1020 insertions(+), 51 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3a386ab..c981de3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ build
 dist
 *.egg-info
 nosetests.xml
+.eggs
diff --git a/.travis.yml b/.travis.yml
index d9c5c7b..5772da7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ python:
   - "3.4"
 env:
   # lucid
-  - HG='1.4.2' BZR=2.1.4 GIT=v1.7.0.4 SVN=1.6.6 PYYAML=3.10
+  # - 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
@@ -47,7 +47,7 @@ install:
   - 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)
+  - cd $HOME/builds && wget https://www.mercurial-scm.org/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
diff --git a/README.rst b/README.rst
index 11cfbef..47f2f53 100644
--- a/README.rst
+++ b/README.rst
@@ -19,23 +19,37 @@ On other Systems, use the pypi package::
 Developer Environment
 ---------------------
 
-source setup.sh to include the src folder in your PYTHONPATH.
+When testing or doing development on vcstools, use a `virtualenv <https://virtualenv.readthedocs.org/en/latest/>`_::
+
+  $ virtualenv ~/vcstools_venv
+  $ source ~/vcstools_venv/bin/activate
+  $ pip install --editable /path/to/vcstools_source
+
+At this point in any shell where you run ``source ~/vcstools_venv/bin/activate``, you can use vcstools and evny edits to files in the vcstools source will take effect immediately.
+This is the effect of ``pip install --editable``, see ``pip install --help``.
+
+To setup a virtualenv for Python3 simply do this (from a clean terminal)::
+
+  $ virtualenv --python=python3 ~/vcstools_venv_py3
+  $ source ~/vcstools_venv_py3
+
+When you're done developing, you can exit any shells where you did ``source .../bin/activate`` and delete the virtualenv folder, e.g. ``~/vcstools_venv``.
 
 Testing
 -------
 
 Use the python library nose to test::
 
-  $ nosetests
+  $ python setup.py test
 
 To test with coverage, make sure to have python-coverage installed and run::
 
+  $ python setup.py test -n  # this installs test dependencies only
   $ nosetests --with-coverage --cover-package vcstools
 
-To run python3 compatibility tests, run either::
+To run python3 compatibility tests, run::
 
-  $ nosetests3
-  $ python3 -m unittest discover --pattern*.py
+  $ python3 setup.py test
 
 Test Status
 -----------
diff --git a/doc/changelog.rst b/doc/changelog.rst
index ab79686..992140c 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -4,6 +4,13 @@ Changelog
 0.1
 ===
 
+0.1.39
+------
+
+- Added support for git submodule in export_repository
+- Add Wily Xenial Yakkety
+- Add get_affected_files for all vcss
+
 0.1.38
 ------
 
diff --git a/setup.py b/setup.py
index ba8d642..0de6a98 100644
--- a/setup.py
+++ b/setup.py
@@ -21,6 +21,8 @@ setup(name='vcstools',
       package_dir={'': 'src'},
       scripts=[],
       install_requires=['pyyaml', 'python-dateutil'],
+      tests_require=['nose', 'mock'],
+      test_suite="nose.collector",
       author="Tully Foote, Thibault Kruse, Ken Conley",
       author_email="tfoote at osrfoundation.org",
       url="http://wiki.ros.org/vcstools",
diff --git a/src/vcstools/__version__.py b/src/vcstools/__version__.py
index b0b983b..a34e4a6 100644
--- a/src/vcstools/__version__.py
+++ b/src/vcstools/__version__.py
@@ -1 +1 @@
-version = '0.1.38'
+version = '0.1.39'
diff --git a/src/vcstools/bzr.py b/src/vcstools/bzr.py
index 6c2ab14..4fb7ec0 100644
--- a/src/vcstools/bzr.py
+++ b/src/vcstools/bzr.py
@@ -228,6 +228,18 @@ class BzrClient(VcsClientBase):
             _, response, _ = run_shell_command(command, shell=True, cwd=basepath)
         return response
 
+    def get_affected_files(self, revision):
+        cmd = "bzr status -c {0} -S -V".format(
+            revision)
+
+        code, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+
+        affected = []
+        if code == 0:
+            for filename in output.splitlines():
+                affected.append(filename.split(" ")[2])
+        return affected
+
     def get_log(self, relpath=None, limit=None):
         response = []
 
diff --git a/src/vcstools/git.py b/src/vcstools/git.py
index 1dd91d7..7301dd0 100644
--- a/src/vcstools/git.py
+++ b/src/vcstools/git.py
@@ -56,13 +56,18 @@ disambiguation, and in some cases warns.
 from __future__ import absolute_import, print_function, unicode_literals
 import os
 import sys
+import shutil
+import tempfile
 import gzip
 import dateutil.parser  # For parsing date strings
 from distutils.version import LooseVersion
+import logging
 
 from vcstools.vcs_base import VcsClientBase, VcsError
 from vcstools.common import sanitized, normalized_rel_path, run_shell_command
 
+from vcstools.git_archive_all import *
+
 
 class GitError(Exception):
     pass
@@ -216,7 +221,7 @@ class GitClient(VcsClientBase):
 
     def _update_submodules(self, verbose=False, timeout=None):
 
-        # update and or init submodules too
+        # update submodules ( and init if necessary ).
         if LooseVersion(self.gitversion) > LooseVersion('1.7'):
             cmd = "git submodule update --init --recursive"
             value, _, _ = run_shell_command(cmd,
@@ -298,8 +303,10 @@ class GitClient(VcsClientBase):
                     (branch_parent, remote) = self._get_branch_parent(current_branch=current_branch)
                     if remote != default_remote:
                         # if remote is not origin, must not fast-forward (because based on origin)
-                        sys.stderr.write("vcstools only handles branches tracking default remote," +
-                                         " branch '%s' tracks remote '%s'\n" % (current_branch, remote))
+                        logger = logging.getLogger('vcstools')
+                        logger.warn("vcstools only handles branches tracking default remote,"
+                                    " branch '%s' tracks remote '%s'.\nRepository path is '%s'."
+                                    % (current_branch, remote, self._path))
                         branch_parent = None
                 # already on correct branch, fast-forward if there is a parent
                 if branch_parent:
@@ -448,6 +455,17 @@ class GitClient(VcsClientBase):
                 response += _git_diff_path_submodule_change(output, rel_path)
         return response
 
+    def get_affected_files(self, revision):
+        cmd = "git show {0} --pretty='format:' --name-only".format(
+            revision)
+        code, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+        affected = []
+        if code == 0:
+            for filename in output.splitlines():
+                if filename not in ('', None, ):
+                    affected.append(filename)
+        return affected
+
     def get_log(self, relpath=None, limit=None):
         response = []
 
@@ -718,23 +736,28 @@ class GitClient(VcsClientBase):
         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:
+        if not self.detect_presence():
             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
+            # since version may relate to remote branch / tag we do not
+            # know about yet, do fetch if not already done
+            self._do_fetch()
+            tmpd_path = tempfile.mkdtemp()
+            try:
+                tmpgit = GitClient(tmpd_path)
+                if tmpgit.checkout(self._path, version=version, shallow=True):
+                    archiver = GitArchiver(main_repo_abspath=tmpgit.get_path(), force_sub=True)
+                    filepath = '{0}.tar.gz'.format(basepath)
+                    archiver.create(filepath)
+                    return filepath
+                else:
+                    return False
+            finally:
+                shutil.rmtree(tmpd_path)
+
+        except GitError:
+            return False
 
     def get_branches(self, local_only=False):
         cmd = 'git branch --no-color'
diff --git a/src/vcstools/git_archive_all.py b/src/vcstools/git_archive_all.py
new file mode 100644
index 0000000..8969b2c
--- /dev/null
+++ b/src/vcstools/git_archive_all.py
@@ -0,0 +1,549 @@
+#! /usr/bin/env python
+# coding=utf-8
+
+# The MIT License (MIT)
+#
+# Copyright (c) 2010 Ilya Kulakov
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# from
+# https://github.com/Kentzo/git-archive-all/blob/497049571f1cfe1c183cd3513b69914fa7379824/git_archive_all.py
+
+
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import logging
+from os import extsep, path, readlink, curdir
+from subprocess import CalledProcessError, Popen, PIPE
+import sys
+import tarfile
+from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+
+__version__ = "1.12"
+
+
+class GitArchiver(object):
+    """
+    GitArchiver
+
+    Scan a git repository and export all tracked files, and submodules.
+    Checks for .gitattributes files in each directory and uses 'export-ignore'
+    pattern entries for ignore files in the archive.
+
+    >>> archiver = GitArchiver(main_repo_abspath='my/repo/path')
+    >>> archiver.create('output.zip')
+    """
+    LOG = logging.getLogger('GitArchiver')
+
+    def __init__(self, prefix='', exclude=True, force_sub=False, extra=None, main_repo_abspath=None):
+        """
+        @param prefix: Prefix used to prepend all paths in the resulting archive.
+            Extra file paths are only prefixed if they are not relative.
+            E.g. if prefix is 'foo' and extra is ['bar', '/baz'] the resulting archive will look like this:
+            /
+              baz
+              foo/
+                bar
+        @type prefix: string
+
+        @param exclude: Determines whether archiver should follow rules specified in .gitattributes files.
+        @type exclude:  bool
+
+        @param force_sub: Determines whether submodules are initialized and updated before archiving.
+        @type force_sub: bool
+
+        @param extra: List of extra paths to include in the resulting archive.
+        @type extra: list
+
+        @param main_repo_abspath: Absolute path to the main repository (or one of subdirectories).
+            If given path is path to a subdirectory (but not a submodule directory!) it will be replaced
+            with abspath to top-level directory of the repository.
+            If None, current cwd is used.
+        @type main_repo_abspath: string
+        """
+        if extra is None:
+            extra = []
+
+        if main_repo_abspath is None:
+            main_repo_abspath = path.abspath('')
+        elif not path.isabs(main_repo_abspath):
+            raise ValueError("You MUST pass absolute path to the main git repository.")
+
+        try:
+            self.run_shell("[ -d .git ] || git rev-parse --git-dir > /dev/null 2>&1", main_repo_abspath)
+        except Exception as e:
+            raise ValueError("{0} not a git repository (or any of the parent directories).".format(main_repo_abspath))
+
+        main_repo_abspath = path.abspath(
+            self.read_git_shell('git rev-parse --show-toplevel', main_repo_abspath)
+            .rstrip()
+        )
+
+        self.prefix = prefix
+        self.exclude = exclude
+        self.extra = extra
+        self.force_sub = force_sub
+        self.main_repo_abspath = main_repo_abspath
+
+    def create(self, output_path, dry_run=False, output_format=None):
+        """
+        Create the archive at output_file_path.
+
+        Type of the archive is determined either by extension of output_file_path or by output_format.
+        Supported formats are: gz, zip, bz2, xz, tar, tgz, txz
+
+        @param output_path: Output file path.
+        @type output_path: string
+
+        @param dry_run: Determines whether create should do nothing but print what it would archive.
+        @type dry_run: bool
+
+        @param output_format: Determines format of the output archive. If None, format is determined from extension
+            of output_file_path.
+        @type output_format: string
+        """
+        if output_format is None:
+            file_name, file_ext = path.splitext(output_path)
+            output_format = file_ext[len(extsep):].lower()
+            self.LOG.debug("Output format is not explicitly set, determined format is {0}.".format(output_format))
+
+        if not dry_run:
+            if output_format == 'zip':
+                archive = ZipFile(path.abspath(output_path), 'w')
+
+                def add_file(file_path, arcname):
+                    if not path.islink(file_path):
+                        archive.write(file_path, arcname, ZIP_DEFLATED)
+                    else:
+                        i = ZipInfo(arcname)
+                        i.create_system = 3
+                        i.external_attr = 0xA1ED0000
+                        archive.writestr(i, readlink(file_path))
+            elif output_format in ['tar', 'bz2', 'gz', 'xz', 'tgz', 'txz']:
+                if output_format == 'tar':
+                    t_mode = 'w'
+                elif output_format == 'tgz':
+                    t_mode = 'w:gz'
+                elif output_format == 'txz':
+                    t_mode = 'w:xz'
+                else:
+                    t_mode = 'w:{0}'.format(output_format)
+
+                archive = tarfile.open(path.abspath(output_path), t_mode)
+
+                def add_file(file_path, arcname):
+                    archive.add(file_path, arcname)
+            else:
+                raise RuntimeError("Unknown format: {0}".format(output_format))
+
+            def archiver(file_path, arcname):
+                self.LOG.debug("Compressing {0} => {1}...".format(file_path, arcname))
+                add_file(file_path, arcname)
+        else:
+            archive = None
+
+            def archiver(file_path, arcname):
+                self.LOG.info("{0} => {1}".format(file_path, arcname))
+
+        self.archive_all_files(archiver)  # this will take care of submodule init and update
+
+        if archive is not None:
+            archive.close()
+
+    def get_exclude_patterns(self, repo_abspath, repo_file_paths):
+        """
+        Returns exclude patterns for a given repo. It looks for .gitattributes files in repo_file_paths.
+
+        Resulting dictionary will contain exclude patterns per path (relative to the repo_abspath).
+        E.g. {('.', 'Catalyst', 'Editions', 'Base'), ['Foo*', '*Bar']}
+
+        @type repo_abspath:     string
+        @param repo_abspath:    Absolute path to the git repository.
+
+        @type repo_file_paths:  list
+        @param repo_file_paths: List of paths relative to the repo_abspath that are under git control.
+
+        @rtype:         dict
+        @return:    Dictionary representing exclude patterns.
+                    Keys are tuples of strings. Values are lists of strings.
+                    Returns None if self.exclude is not set.
+        """
+        if not self.exclude:
+            return None
+
+        def read_attributes(attributes_abspath):
+            patterns = []
+            if path.isfile(attributes_abspath):
+                attributes = open(attributes_abspath, 'r').readlines()
+                patterns = []
+                for line in attributes:
+                    tokens = line.strip().split()
+                    if "export-ignore" in tokens[1:]:
+                        patterns.append(tokens[0])
+            return patterns
+
+        exclude_patterns = {(): []}
+
+        # There may be no gitattributes.
+        try:
+            global_attributes_abspath = self.read_shell("git config --get core.attributesfile", repo_abspath).rstrip()
+            exclude_patterns[()] = read_attributes(global_attributes_abspath)
+        except:
+            # And it's valid to not have them.
+            pass
+
+        for attributes_abspath in [path.join(repo_abspath, f) for f in repo_file_paths if f.endswith(".gitattributes")]:
+            # Each .gitattributes affects only files within its directory.
+            key = tuple(self.get_path_components(repo_abspath, path.dirname(attributes_abspath)))
+            exclude_patterns[key] = read_attributes(attributes_abspath)
+
+        local_attributes_abspath = path.join(repo_abspath, ".git", "info", "attributes")
+        key = tuple(self.get_path_components(repo_abspath, repo_abspath))
+
+        if key in exclude_patterns:
+            exclude_patterns[key].extend(read_attributes(local_attributes_abspath))
+        else:
+            exclude_patterns[key] = read_attributes(local_attributes_abspath)
+
+        return exclude_patterns
+
+    def is_file_excluded(self, repo_abspath, repo_file_path, exclude_patterns):
+        """
+        Checks whether file at a given path is excluded.
+
+        @type repo_abspath: string
+        @param repo_abspath: Absolute path to the git repository.
+
+        @type repo_file_path:   string
+        @param repo_file_path:  Path to a file within repo_abspath.
+
+        @type exclude_patterns:     dict
+        @param exclude_patterns:    Exclude patterns with format specified for get_exclude_patterns.
+
+        @rtype: bool
+        @return: True if file should be excluded. Otherwise False.
+        """
+        if exclude_patterns is None or not len(exclude_patterns):
+            return False
+
+        from fnmatch import fnmatch
+
+        file_name = path.basename(repo_file_path)
+        components = self.get_path_components(repo_abspath, path.join(repo_abspath, path.dirname(repo_file_path)))
+
+        is_excluded = False
+        # We should check all patterns specified in intermediate directories to the given file.
+        # At the end we should also check for the global patterns (key '()' or empty tuple).
+        while not is_excluded:
+            key = tuple(components)
+            if key in exclude_patterns:
+                patterns = exclude_patterns[key]
+                for p in patterns:
+                    if fnmatch(file_name, p) or fnmatch(repo_file_path, p):
+                        self.LOG.debug("Exclude pattern matched {0}: {1}".format(p, repo_file_path))
+                        is_excluded = True
+
+            if not len(components):
+                break
+
+            components.pop()
+
+        return is_excluded
+
+    def archive_all_files(self, archiver):
+        """
+        Archive all files using archiver.
+
+        @param archiver: Callable that accepts 2 arguments:
+            abspath to file on the system and relative path within archive.
+        """
+        for file_path in self.extra:
+            archiver(path.abspath(file_path), path.join(self.prefix, file_path))
+
+        for file_path in self.walk_git_files():
+            archiver(path.join(self.main_repo_abspath, file_path), path.join(self.prefix, file_path))
+
+    def walk_git_files(self, repo_path=''):
+        """
+        An iterator method that yields a file path relative to main_repo_abspath
+        for each file that should be included in the archive.
+        Skips those that match the exclusion patterns found in
+        any discovered .gitattributes files along the way.
+
+        Recurs into submodules as well.
+
+        @type repo_path:    string
+        @param repo_path:   Path to the git submodule repository relative to main_repo_abspath.
+
+        @rtype:     iterator
+        @return:    Iterator to traverse files under git control relative to main_repo_abspath.
+        """
+        repo_abspath = path.join(self.main_repo_abspath, repo_path)
+        repo_file_paths = self.read_git_shell(
+            "git ls-files --cached --full-name --no-empty-directory",
+            repo_abspath
+        ).splitlines()
+        exclude_patterns = self.get_exclude_patterns(repo_abspath, repo_file_paths)
+
+        for repo_file_path in repo_file_paths:
+            # Git puts path in quotes if file path has unicode characters.
+            repo_file_path = repo_file_path.strip('"')  # file path relative to current repo
+            file_name = path.basename(repo_file_path)
+            main_repo_file_path = path.join(repo_path, repo_file_path)  # file path relative to the main repo
+
+            # Only list symlinks and files that don't start with git.
+            if file_name.startswith(".git") or (
+                not path.islink(main_repo_file_path) and path.isdir(main_repo_file_path)
+            ):
+                continue
+
+            if self.is_file_excluded(repo_abspath, repo_file_path, exclude_patterns):
+                continue
+
+            yield main_repo_file_path
+
+        if self.force_sub:
+            self.run_shell("git submodule init", repo_abspath)
+            self.run_shell("git submodule update", repo_abspath)
+
+        for submodule_path in self.read_shell("git submodule --quiet foreach 'pwd -P'", repo_abspath).splitlines():
+            # Shell command returns absolute paths to submodules.
+            submodule_path = path.relpath(submodule_path, self.main_repo_abspath)
+            for file_path in self.walk_git_files(submodule_path):
+                yield file_path
+
+    @staticmethod
+    def get_path_components(repo_abspath, abspath):
+        """
+        Split given abspath into components relative to repo_abspath.
+        These components are primarily used as unique keys of files and folders within a repository.
+
+        E.g. if repo_abspath is '/Documents/Hobby/ParaView/' and abspath is
+        '/Documents/Hobby/ParaView/Catalyst/Editions/Base/', function will return:
+        ['.', 'Catalyst', 'Editions', 'Base']
+
+        First element is always '.' (concrete symbol depends on OS).
+
+        @param repo_abspath: Absolute path to the git repository. Normalized via os.path.normpath.
+        @type repo_abspath: string
+
+        @param abspath: Absolute path to a file within repo_abspath. Normalized via os.path.normpath.
+        @type abspath: string
+
+        @return: List of path components.
+        @rtype: list
+        """
+        repo_abspath = path.normpath(repo_abspath)
+        abspath = path.normpath(abspath)
+
+        if not path.isabs(repo_abspath):
+            raise ValueError("repo_abspath MUST be absolute path.")
+
+        if not path.isabs(abspath):
+            raise ValueError("abspath MUST be absoulte path.")
+
+        if not path.commonprefix([repo_abspath, abspath]):
+            raise ValueError(
+                "abspath (\"{0}\") MUST have common prefix with repo_abspath (\"{1}\")"
+                .format(abspath, repo_abspath)
+            )
+
+        components = []
+
+        while not abspath == repo_abspath:
+            abspath, tail = path.split(abspath)
+
+            if tail:
+                components.insert(0, tail)
+
+        components.insert(0, curdir)
+        return components
+
+    @staticmethod
+    def run_shell(cmd, cwd=None):
+        """
+        Runs shell command.
+
+        @type cmd:  string
+        @param cmd: Command to be executed.
+
+        @type cwd:  string
+        @param cwd: Working directory.
+
+        @rtype:     int
+        @return:    Return code of the command.
+
+        @raise CalledProcessError:  Raises exception if return code of the command is non-zero.
+        """
+        p = Popen(cmd, shell=True, cwd=cwd)
+        p.wait()
+
+        if p.returncode:
+            raise CalledProcessError(returncode=p.returncode, cmd=cmd)
+
+        return p.returncode
+
+    @staticmethod
+    def read_shell(cmd, cwd=None, encoding='utf-8'):
+        """
+        Runs shell command and reads output.
+
+        @type cmd:  string
+        @param cmd: Command to be executed.
+
+        @type cwd:  string
+        @param cwd: Working directory.
+
+        @type encoding: string
+        @param encoding: Encoding used to decode bytes returned by Popen into string.
+
+        @rtype:     string
+        @return:    Output of the command.
+
+        @raise CalledProcessError:  Raises exception if return code of the command is non-zero.
+        """
+        p = Popen(cmd, shell=True, stdout=PIPE, cwd=cwd)
+        output, _ = p.communicate()
+        output = output.decode(encoding)
+
+        if p.returncode:
+            if sys.version_info > (2, 6):
+                raise CalledProcessError(returncode=p.returncode, cmd=cmd, output=output)
+            else:
+                raise CalledProcessError(returncode=p.returncode, cmd=cmd)
+
+        return output
+
+    @staticmethod
+    def read_git_shell(cmd, cwd=None):
+        """
+        Runs git shell command, reads output and decodes it into unicode string
+
+        @type cmd:  string
+        @param cmd: Command to be executed.
+
+        @type cwd:  string
+        @param cwd: Working directory.
+
+        @rtype:     string
+        @return:    Output of the command.
+
+        @raise CalledProcessError:  Raises exception if return code of the command is non-zero.
+        """
+        p = Popen(cmd, shell=True, stdout=PIPE, cwd=cwd)
+        output, _ = p.communicate()
+        output = output.decode('unicode_escape').encode('raw_unicode_escape').decode('utf-8')
+
+        if p.returncode:
+            if sys.version_info > (2, 6):
+                raise CalledProcessError(returncode=p.returncode, cmd=cmd, output=output)
+            else:
+                raise CalledProcessError(returncode=p.returncode, cmd=cmd)
+
+        return output
+
+
+def main():
+    from optparse import OptionParser
+
+    parser = OptionParser(
+        usage="usage: %prog [-v] [--prefix PREFIX] [--no-exclude] [--force-submodules]"
+              " [--extra EXTRA1 [EXTRA2]] [--dry-run] OUTPUT_FILE",
+        version="%prog {0}".format(__version__)
+    )
+
+    parser.add_option('--prefix',
+                      type='string',
+                      dest='prefix',
+                      default=None,
+                      help="""prepend PREFIX to each filename in the archive.
+                          OUTPUT_FILE name is used by default to avoid tarbomb.
+                          You can set it to '' in order to explicitly request tarbomb""")
+
+    parser.add_option('-v', '--verbose',
+                      action='store_true',
+                      dest='verbose',
+                      help='enable verbose mode')
+
+    parser.add_option('--no-exclude',
+                      action='store_false',
+                      dest='exclude',
+                      default=True,
+                      help="don't read .gitattributes files for patterns containing export-ignore attrib")
+
+    parser.add_option('--force-submodules',
+                      action='store_true',
+                      dest='force_sub',
+                      help='force a git submodule init && git submodule update at each level before iterating submodules')
+
+    parser.add_option('--extra',
+                      action='append',
+                      dest='extra',
+                      default=[],
+                      help="any additional files to include in the archive")
+
+    parser.add_option('--dry-run',
+                      action='store_true',
+                      dest='dry_run',
+                      help="don't actually archive anything, just show what would be done")
+
+    options, args = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("You must specify exactly one output file")
+
+    output_file_path = args[0]
+
+    if path.isdir(output_file_path):
+        parser.error("You cannot use directory as output")
+
+    # avoid tarbomb
+    if options.prefix is not None:
+        options.prefix = path.join(options.prefix, '')
+    else:
+        import re
+
+        output_name = path.basename(output_file_path)
+        output_name = re.sub(
+            '(\.zip|\.tar|\.tgz|\.txz|\.gz|\.bz2|\.xz|\.tar\.gz|\.tar\.bz2|\.tar\.xz)$',
+            '',
+            output_name
+        ) or "Archive"
+        options.prefix = path.join(output_name, '')
+
+    try:
+        handler = logging.StreamHandler(sys.stdout)
+        handler.setFormatter(logging.Formatter('%(message)s'))
+        GitArchiver.LOG.addHandler(handler)
+        GitArchiver.LOG.setLevel(logging.DEBUG if options.verbose else logging.INFO)
+        archiver = GitArchiver(options.prefix,
+                               options.exclude,
+                               options.force_sub,
+                               options.extra)
+        archiver.create(output_file_path, options.dry_run)
+    except Exception as e:
+        parser.exit(2, "{0}\n".format(e))
+
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/vcstools/hg.py b/src/vcstools/hg.py
index a776685..a38036e 100644
--- a/src/vcstools/hg.py
+++ b/src/vcstools/hg.py
@@ -276,6 +276,14 @@ class HgClient(VcsClientBase):
             response = _hg_diff_path_change(response, rel_path)
         return response
 
+    def get_affected_files(self, revision):
+        cmd = "hg log -r %s --template '{files}'" % revision
+        code, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+        affected = []
+        if code == 0:
+            affected = output.split(" ")
+        return affected
+
     def get_log(self, relpath=None, limit=None):
         response = []
 
diff --git a/src/vcstools/svn.py b/src/vcstools/svn.py
index e338f39..e330872 100755
--- a/src/vcstools/svn.py
+++ b/src/vcstools/svn.py
@@ -291,6 +291,17 @@ class SvnClient(VcsClientBase):
                                                cwd=basepath)
         return response
 
+    def get_affected_files(self, revision):
+        cmd = "svn diff --summarize -c {0}".format(
+            revision)
+
+        code, output, _ = run_shell_command(cmd, shell=True, cwd=self._path)
+        affected = []
+        if code == 0:
+            for filename in output.splitlines():
+                affected.append(filename.split(" ")[7])
+        return affected
+
     def get_log(self, relpath=None, limit=None):
         response = []
 
diff --git a/src/vcstools/tar.py b/src/vcstools/tar.py
index 85fe9d4..d27df9e 100644
--- a/src/vcstools/tar.py
+++ b/src/vcstools/tar.py
@@ -110,6 +110,7 @@ class TarClient(VcsClientBase):
             temp_tarfile = tarfile.open(filename, 'r:*')
             members = None  # means all members in extractall
             if version == '' or version is None:
+                subdir = tempdir
                 self.logger.warn("No tar subdirectory chosen via the 'version' argument for url: %s" % url)
             else:
                 # getmembers lists all files contained in tar with
@@ -123,9 +124,9 @@ class TarClient(VcsClientBase):
                         subdirs.append(m.name.split('/')[0])
                 if not members:
                     raise VcsError("%s is not a subdirectory with contents in members %s" % (version, subdirs))
+                subdir = os.path.join(tempdir, version)
             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)
 
@@ -153,9 +154,9 @@ class TarClient(VcsClientBase):
         """
         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")
+            sys.stderr.write("Tarball Client does not support updating with different version '%s' != '%s'\n"
+                             % (version, self.get_version()))
             return False
 
         return True
diff --git a/src/vcstools/vcs_base.py b/src/vcstools/vcs_base.py
index 7b67429..9448fec 100644
--- a/src/vcstools/vcs_base.py
+++ b/src/vcstools/vcs_base.py
@@ -254,6 +254,15 @@ class VcsClientBase(object):
         raise NotImplementedError("Base class get_status method must be overridden for client type %s " %
                                   self._vcs_type_name)
 
+    def get_affected_files(self, revision):
+        """
+        Get the files that were affected by a specific revision
+        :param revision: SHA or revision number.
+        :returns: A list of strings with the files affected by a specific commit
+        """
+        raise NotImplemented(
+            "Base class get_affected_files method must be overriden")
+
     def get_log(self, relpath=None, limit=None):
         """
         Calls scm log command.
diff --git a/stdeb.cfg b/stdeb.cfg
index a166317..832b099 100644
--- a/stdeb.cfg
+++ b/stdeb.cfg
@@ -1,5 +1,5 @@
 [DEFAULT]
 Depends: subversion, mercurial, git-core, bzr, python-yaml, python-dateutil
 Depends3: subversion, mercurial, git-core, bzr, python3-yaml
-Suite: oneiric precise quantal raring saucy trusty utopic vivid wheezy jessie
+Suite: oneiric precise quantal raring saucy trusty utopic vivid wily xenial yakkety wheezy jessie
 X-Python3-Version: >= 3.2
diff --git a/test/test_bzr.py b/test/test_bzr.py
index 09a6be5..e9f67cf 100644
--- a/test/test_bzr.py
+++ b/test/test_bzr.py
@@ -260,6 +260,23 @@ class BzrClientLogTest(BzrClientTestSetups):
         self.assertEquals('initial', log[0]['message'])
 
 
+class BzrClientAffectedFilesTest(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(limit=1)[0]
+        affected = client.get_affected_files(log['id'])
+        self.assertEqual(sorted(['deleted-fs.txt', 'deleted.txt']),
+                         sorted(affected))
+
+
 class BzrDiffStatClientTest(BzrClientTestSetups):
 
     @classmethod
diff --git a/test/test_code_format.py b/test/test_code_format.py
index 066fb6b..fb98c00 100644
--- a/test/test_code_format.py
+++ b/test/test_code_format.py
@@ -24,6 +24,7 @@ def test_pep8_conformance():
     pep8style = pep8.StyleGuide(max_line_length=120)
     report = pep8style.options.report
     report.start()
+    pep8style.options.exclude.append('git_archive_all.py')
     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
index e22c503..af6dc4c 100644
--- a/test/test_git.py
+++ b/test/test_git.py
@@ -595,6 +595,28 @@ class GitClientLogTest(GitClientTestSetups):
             self.assertEquals(1, len(log))
 
 
+class GitClientAffectedFiles(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_file", shell=True, cwd=self.local_path)
+        subprocess.check_call("git add local_file", shell=True, cwd=self.local_path)
+        subprocess.check_call("git commit -m \"local_file\"", shell=True, cwd=self.local_path)
+
+    def test_get_affected_files(self):
+        client = GitClient(self.local_path)
+        affected = client.get_affected_files(client.get_log()[0]['id'])
+
+        self.assertEqual(sorted(['local_file']),
+                         sorted(affected))
+
+        self.assertEquals(['local_file'], affected)
+
+
 class GitClientDanglingCommitsTest(GitClientTestSetups):
 
     def setUp(self):
diff --git a/test/test_git_subm.py b/test/test_git_subm.py
index 97ffa83..a5354f4 100644
--- a/test/test_git_subm.py
+++ b/test/test_git_subm.py
@@ -38,8 +38,12 @@ import unittest
 import subprocess
 import tempfile
 import shutil
+import tarfile
+import filecmp
+from contextlib import closing
+from distutils.version import LooseVersion
 
-from vcstools.git import GitClient
+from vcstools.git import GitClient, _get_git_version
 
 
 class GitClientTestSetups(unittest.TestCase):
@@ -57,6 +61,12 @@ class GitClientTestSetups(unittest.TestCase):
         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")
+        self.subsublocal2_path = os.path.join(self.sublocal2_path, "subsubmodule")
+        self.export_path = os.path.join(self.root_directory, "export")
+        self.subexport_path = os.path.join(self.export_path, "submodule")
+        self.subexport2_path = os.path.join(self.export_path, "submodule2")
+        self.subsubexport_path = os.path.join(self.subexport_path, "subsubmodule")
+        self.subsubexport2_path = os.path.join(self.subexport2_path, "subsubmodule")
         os.makedirs(self.remote_path)
         os.makedirs(self.submodule_path)
         os.makedirs(self.subsubmodule_path)
@@ -67,8 +77,9 @@ class GitClientTestSetups(unittest.TestCase):
         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)
+        subprocess.check_call("git branch initial_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
@@ -92,41 +103,57 @@ class GitClientTestSetups(unittest.TestCase):
         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)
+        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)
+        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)
+        # attach submodule somewhere, only in test_branch first
+        subprocess.check_call("git checkout master -b test_branch", shell=True, cwd=self.remote_path)
+        subprocess.check_call("git submodule add %s %s" % (self.submodule_path,
+                                                           "submodule2"), shell=True, cwd=self.remote_path)
+
+        # this is needed only if git <= 1.7, during the time when submodules were being introduced (from 1.5.3)
         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)
+        po = subprocess.Popen("git log -n 1 --pretty=format:\"%H\"", shell=True,
+                              cwd=self.remote_path, stdout=subprocess.PIPE)
+        self.version_test = po.stdout.read().decode('UTF-8').rstrip('"').lstrip('"')
+
+        # attach submodule to remote on master. CAREFUL : submodule2 is still in working tree (git does not clean it)
+        subprocess.check_call("git checkout master", shell=True, cwd=self.remote_path)
+        subprocess.check_call("git submodule add %s %s" % (self.submodule_path, "submodule"),
+                              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)
+        # this is needed only if git <= 1.7, during the time when submodules were being introduced (from 1.5.3)
         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)
+        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)
 
     @classmethod
     def tearDownClass(self):
         for d in self.directories:
             shutil.rmtree(self.directories[d])
+        pass
 
     def tearDown(self):
         if os.path.exists(self.local_path):
             shutil.rmtree(self.local_path)
+        if os.path.exists(self.export_path):
+            shutil.rmtree(self.export_path)
+        pass
 
 
 class GitClientTest(GitClientTestSetups):
@@ -149,18 +176,191 @@ class GitClientTest(GitClientTestSetups):
         self.assertTrue(subsubclient.detect_presence())
         self.assertEqual(self.subsubversion_final, subsubclient.get_version())
 
-    def test_checkout_branch_with_subs(self):
+    def test_export_master(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.assertFalse(os.path.exists(self.export_path))
+        self.assertTrue(client.checkout(url))
+        self.assertTrue(client.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        tarpath = client.export_repository("master", self.export_path)
+        self.assertEqual(tarpath, self.export_path + '.tar.gz')
+        os.mkdir(self.export_path)
+        with closing(tarfile.open(tarpath, "r:gz")) as tarf:
+            tarf.extractall(self.export_path)
+        subsubdirdiff = filecmp.dircmp(self.subsubexport_path, self.subsublocal_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(subsubdirdiff.left_only, [])
+        self.assertEqual(subsubdirdiff.right_only, [])
+        self.assertEqual(subsubdirdiff.diff_files, [])
+        subdirdiff = filecmp.dircmp(self.subexport_path, self.sublocal_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(subdirdiff.left_only, [])
+        self.assertEqual(subdirdiff.right_only, [])
+        self.assertEqual(subdirdiff.diff_files, [])
+        dirdiff = filecmp.dircmp(self.export_path, self.local_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(dirdiff.left_only, [])
+        self.assertEqual(dirdiff.right_only, [])
+        self.assertEqual(dirdiff.diff_files, [])
+
+    def test_export_branch(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)
+        subsubclient2 = GitClient(self.subsublocal2_path)
+        self.assertFalse(client.path_exists())
+        self.assertFalse(client.detect_presence())
+        self.assertFalse(os.path.exists(self.export_path))
+        self.assertTrue(client.checkout(url, version='master'))
+        self.assertTrue(client.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        self.assertFalse(subclient2.path_exists())
+        self.assertFalse(subsubclient2.path_exists())
+        # we need first to retrieve locally the branch we want to export
+        self.assertTrue(client.update(version='test_branch'))
+        self.assertTrue(client.path_exists())
+        # git leaves old submodule around by default
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        # new submodule should be there
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+
+        tarpath = client.export_repository("test_branch", self.export_path)
+        self.assertEqual(tarpath, self.export_path + '.tar.gz')
+        os.mkdir(self.export_path)
+        with closing(tarfile.open(tarpath, "r:gz")) as tarf:
+            tarf.extractall(self.export_path)
+
+        # Checking that we have only submodule2 in our export
+        self.assertFalse(os.path.exists(self.subexport_path))
+        self.assertFalse(os.path.exists(self.subsubexport_path))
+        self.assertTrue(os.path.exists(self.subexport2_path))
+        self.assertTrue(os.path.exists(self.subsubexport2_path))
+
+        # comparing with test_branch version ( currently checked-out )
+        subsubdirdiff = filecmp.dircmp(self.subsubexport2_path, self.subsublocal_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(subsubdirdiff.left_only, [])  # same subsubfixed.txt in both subsubmodule/
+        self.assertEqual(subsubdirdiff.right_only, [])
+        self.assertEqual(subsubdirdiff.diff_files, [])
+        subdirdiff = filecmp.dircmp(self.subexport2_path, self.sublocal_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(subdirdiff.left_only, [])
+        self.assertEqual(subdirdiff.right_only, [])
+        self.assertEqual(subdirdiff.diff_files, [])
+        dirdiff = filecmp.dircmp(self.export_path, self.local_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(dirdiff.left_only, [])
+        # submodule is still there on local_path (git default behavior)
+        self.assertEqual(dirdiff.right_only, ['submodule'])
+        self.assertEqual(dirdiff.diff_files, [])
+
+    def test_export_hash(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)
+        subsubclient2 = GitClient(self.subsublocal2_path)
+        self.assertFalse(client.path_exists())
+        self.assertFalse(client.detect_presence())
+        self.assertFalse(os.path.exists(self.export_path))
+        self.assertTrue(client.checkout(url, version='master'))
+        self.assertTrue(client.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        self.assertFalse(subclient2.path_exists())
+        self.assertFalse(subsubclient2.path_exists())
+        # we need first to retrieve locally the hash we want to export
+        self.assertTrue(client.update(version=self.version_test))
+        self.assertTrue(client.path_exists())
+        # git leaves old submodule around by default
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        # new submodule should be there
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+
+        tarpath = client.export_repository(self.version_test, self.export_path)
+        self.assertEqual(tarpath, self.export_path + '.tar.gz')
+        os.mkdir(self.export_path)
+        with closing(tarfile.open(tarpath, "r:gz")) as tarf:
+            tarf.extractall(self.export_path)
+
+        # Checking that we have only submodule2 in our export
+        self.assertFalse(os.path.exists(self.subexport_path))
+        self.assertFalse(os.path.exists(self.subsubexport_path))
+        self.assertTrue(os.path.exists(self.subexport2_path))
+        self.assertTrue(os.path.exists(self.subsubexport2_path))
+
+        # comparing with version_test ( currently checked-out )
+        subsubdirdiff = filecmp.dircmp(self.subsubexport2_path, self.subsublocal_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(subsubdirdiff.left_only, [])  # same subsubfixed.txt in both subsubmodule/
+        self.assertEqual(subsubdirdiff.right_only, [])
+        self.assertEqual(subsubdirdiff.diff_files, [])
+        subdirdiff = filecmp.dircmp(self.subexport2_path, self.sublocal_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(subdirdiff.left_only, [])
+        self.assertEqual(subdirdiff.right_only, [])
+        self.assertEqual(subdirdiff.diff_files, [])
+        dirdiff = filecmp.dircmp(self.export_path, self.local_path, ignore=['.git', '.gitmodules'])
+        self.assertEqual(dirdiff.left_only, [])
+        # submodule is still there on local_path (git default behavior)
+        self.assertEqual(dirdiff.right_only, ['submodule'])
+        self.assertEqual(dirdiff.diff_files, [])
+
+    def test_checkout_branch_without_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='initial_branch'))
         self.assertTrue(client.path_exists())
         self.assertTrue(client.detect_presence())
         self.assertEqual(self.version_init, client.get_version())
         self.assertFalse(subclient.path_exists())
+        self.assertFalse(subsubclient.path_exists())
+
+    def test_checkout_test_branch_with_subs(self):
+        url = self.remote_path
+        client = GitClient(self.local_path)
+        subclient = GitClient(self.sublocal_path)
+        subsubclient = GitClient(self.subsublocal_path)
+        subclient2 = GitClient(self.sublocal2_path)
+        subsubclient2 = GitClient(self.subsublocal2_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_test, client.get_version())
+        self.assertFalse(subclient.path_exists())
+        self.assertFalse(subsubclient.path_exists())
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+
+    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)
+        subclient2 = GitClient(self.sublocal2_path)
+        subsubclient2 = GitClient(self.subsublocal2_path)
+        self.assertFalse(client.path_exists())
+        self.assertFalse(client.detect_presence())
+        self.assertTrue(client.checkout(url, version='master'))
+        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(subsubclient.path_exists())
+        self.assertFalse(subclient2.path_exists())
+        self.assertFalse(subsubclient2.path_exists())
 
     def test_switch_branches(self):
         url = self.remote_path
@@ -168,6 +368,36 @@ class GitClientTest(GitClientTestSetups):
         subclient = GitClient(self.sublocal_path)
         subclient2 = GitClient(self.sublocal2_path)
         subsubclient = GitClient(self.subsublocal_path)
+        subsubclient2 = GitClient(self.subsublocal2_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_branch"
+        self.assertTrue(client.update(new_version))
+        # checking that update doesnt make submodule disappear (git default behavior)
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        oldnew_version = "master"
+        self.assertTrue(client.update(oldnew_version))
+        # checking that update doesnt make submodule2 disappear (git default behavior)
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+
+    def test_switch_branches_retrieve_local_subcommit(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)
+        subsubclient2 = GitClient(self.subsublocal2_path)
         self.assertFalse(client.path_exists())
         self.assertFalse(client.detect_presence())
         self.assertTrue(client.checkout(url))
@@ -175,9 +405,32 @@ class GitClientTest(GitClientTestSetups):
         self.assertTrue(subclient.path_exists())
         self.assertTrue(subsubclient.path_exists())
         self.assertFalse(subclient2.path_exists())
-        new_version = "test_branch2"
+        new_version = "test_branch"
+        self.assertTrue(client.update(new_version))
+        # checking that update doesnt make submodule disappear (git default behavior)
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        subprocess.check_call("touch submodif.txt", shell=True, cwd=self.sublocal2_path)
+        subprocess.check_call("git add submodif.txt", shell=True, cwd=self.sublocal2_path)
+        subprocess.check_call("git commit -m submodif", shell=True, cwd=self.sublocal2_path)
+        subprocess.check_call("git add submodule2", shell=True, cwd=self.local_path)
+        subprocess.check_call("git commit -m submodule2_modif", shell=True, cwd=self.local_path)
+        oldnew_version = "master"
+        self.assertTrue(client.update(oldnew_version))
+        # checking that update doesnt make submodule2 disappear (git default behavior)
+        self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
         self.assertTrue(client.update(new_version))
+        # checking that update still has submodule with submodif
         self.assertTrue(subclient2.path_exists())
+        self.assertTrue(subsubclient2.path_exists())
+        self.assertTrue(subclient.path_exists())
+        self.assertTrue(subsubclient.path_exists())
+        self.assertTrue(os.path.exists(os.path.join(self.sublocal2_path, "submodif.txt")))
 
     def test_status(self):
         url = self.remote_path
@@ -197,10 +450,12 @@ class GitClientTest(GitClientTestSetups):
         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())
+        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())
+        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())
diff --git a/test/test_hg.py b/test/test_hg.py
index 6770112..f45a7c6 100644
--- a/test/test_hg.py
+++ b/test/test_hg.py
@@ -276,6 +276,25 @@ class HGClientLogTest(HGClientTestSetups):
         self.assertEquals('initial', log[0]['message'])
 
 
+class HGAffectedFilesTest(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(limit=1)[0]
+        affected = client.get_affected_files(log['id'])
+
+        self.assertEqual(sorted(['deleted-fs.txt', 'deleted.txt']),
+                         sorted(affected))
+
+
+
 class HGDiffStatClientTest(HGClientTestSetups):
 
     @classmethod
diff --git a/test/test_svn.py b/test/test_svn.py
index 7035d55..ee4f067 100644
--- a/test/test_svn.py
+++ b/test/test_svn.py
@@ -322,6 +322,24 @@ class SvnClientLogTest(SvnClientTestSetups):
         self.assertEquals('initial', log[0]['message'])
 
 
+class SVNClientAffectedFiles(SvnClientTestSetups):
+
+    @classmethod
+    def setUpClass(self):
+        SvnClientTestSetups.setUpClass()
+        client = SvnClient(self.local_path)
+        client.checkout(self.local_url)
+
+    def test_get_affected_files(self):
+        client = SvnClient(self.local_path)
+        client.checkout(self.local_url)
+        log = client.get_log(limit=1)[0]
+        affected = client.get_affected_files(log['id'])
+
+        self.assertEqual(sorted(['deleted-fs.txt', 'deleted.txt']),
+                         sorted(affected))
+
+
 class SvnDiffStatClientTest(SvnClientTestSetups):
 
     @classmethod

-- 
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