Openstack Swift backend support for bare repositories
authorFabien Boucher <fabien.boucher@enovance.com>
Wed, 7 May 2014 16:18:37 +0000 (18:18 +0200)
committerJelmer Vernooij <jelmer@samba.org>
Sat, 7 Jun 2014 00:12:10 +0000 (02:12 +0200)
This patch adds a new SwiftRepo object that inherits of
BaseRepo. The SwiftRepo object knows how to deal with
git repositories stored on an Openstack object storage
Swift.

Regular filesystems are the defacto backend for Git
repositories but suffers of some limitations in term of
availability and scalability. Using Swift as backend, Git
objects are duplicated in multiple replica, that improves a lot
the disponibity of your repositories, avoids the downtime
due to infrastructure maintenance and also avoids the need to
configure backups. Extending a regular filesystem where
your repositories are stored can be risky and can leads to
downtime. With Swift as backend, extending the storage size
is transparent.

For more details have a look to the README.swift file
provided by this patch.

README.swift [new file with mode: 0644]
bin/dulwich
dulwich/server.py
dulwich/swift.py [new file with mode: 0644]
dulwich/tests/__init__.py
dulwich/tests/test_swift.py [new file with mode: 0644]
dulwich/tests_swift/__init__.py [new file with mode: 0644]
dulwich/tests_swift/test_smoke.py [new file with mode: 0644]

diff --git a/README.swift b/README.swift
new file mode 100644 (file)
index 0000000..ac7d8d6
--- /dev/null
@@ -0,0 +1,132 @@
+Openstack Swift as backend for Dulwich
+======================================
+
+The module dulwich/swift.py implements dulwich.repo.BaseRepo
+in order to being compatible with Openstack Swift.
+We can then use Dulwich as server (Git server) and instead of using
+a regular POSIX file system to store repository objects we use the
+object storage Swift via its own API.
+
+c Git client <---> Dulwich server <---> Openstack Swift API
+
+This implementation is still a work in progress and we can say that
+is a Beta version so you need to be prepared to find bugs.
+
+Configuration file
+------------------
+
+We need to provide some configuration values in order to let Dulwich
+talk and authenticate against Swift. The following config file must
+be used as template:
+
+    [swift]
+    # Authentication URL (Keystone or Swift)
+    auth_url = http://127.0.0.1:5000/v2.0
+    # Authentication version to use
+    auth_ver = 2
+    # The tenant and username separated by a semicolon
+    username = admin;admin
+    # The user password
+    password = pass
+    # The Object storage region to use (auth v2) (Default RegionOne)
+    region_name = RegionOne
+    # The Object storage endpoint URL to use (auth v2) (Default internalURL)
+    endpoint_type = internalURL
+    # Concurrency to use for parallel tasks (Default 10)
+    concurrency = 10
+    # Size of the HTTP pool (Default 10)
+    http_pool_length = 10
+    # Timeout delay for HTTP connections (Default 20)
+    http_timeout = 20
+    # Chunk size to read from pack (Bytes) (Default 12228)
+    chunk_length = 12228
+    # Cache size (MBytes) (Default 20)
+    cache_length = 20
+
+
+Note that for now we use the same tenant to perform the requests
+against Swift. Therefor there is only one Swift account used
+for storing repositories. Each repository will be contained in
+a Swift container.
+
+How to start unittest
+---------------------
+
+There is no need to have a Swift cluster running to run the unitests.
+Just run the following command in the Dulwich source directory:
+
+    $ PYTHONPATH=. nosetests dulwich/tests/test_swift.py
+
+How to start functional tests
+-----------------------------
+
+We provide some basic tests to perform smoke tests against a real Swift
+cluster. To run those functional tests you need a properly configured
+configuration file. The tests can be run as follow:
+
+    $ DULWICH_SWIFT_CFG=/etc/swift-dul.conf PYTHONPATH=. nosetests dulwich/tests_swift/test_smoke.py
+
+How to install
+--------------
+
+Install the Dulwich library via the setup.py. The dependencies will be
+automatically retrieved from pypi:
+
+    $ python ./setup.py install
+
+How to run the server
+---------------------
+
+Start the server using the following command:
+
+    $ dul-daemon -c /etc/swift-dul.conf -l 127.0.0.1 --backend=swift
+
+Note that a lot of request will be performed against the Swift
+cluster so it is better to start the Dulwich server as close
+as possible of the Swift proxy. The best solution is to run
+the server on the Swift proxy node to reduce the latency.
+
+How to use
+----------
+
+Once you have validated that the functional tests is working as expected and
+the server is running we can init a bare repository. Run this
+command with the name of the repository to create:
+
+    $ dulwich init-swift -c /etc/swift-dul.conf edeploy
+
+The repository name will be the container that will contain all the Git
+objects for the repository. Then standard c Git client can be used to
+perform operations againt this repository.
+
+As an example we can clone the previously empty bare repository:
+
+    $ git clone git://localhost/edeploy
+
+Then push an existing project in it:
+
+    $ git clone https://github.com/enovance/edeploy.git edeployclone
+    $ cd edeployclone
+    $ git remote add alt git://localhost/edeploy
+    $ git push alt master
+    $ git ls-remote alt
+    9dc50a9a9bff1e232a74e365707f22a62492183e        HEAD
+    9dc50a9a9bff1e232a74e365707f22a62492183e        refs/heads/master
+
+The other Git commands can be used the way you do usually against
+a regular repository.
+
+Note the swift-dul-daemon start a Git server listening for the
+Git protocol. Therefor there ins't any authentication or encryption
+at all between the cGIT client and the GIT server (Dulwich).
+
+Note on the .info file for pack object
+--------------------------------------
+
+The Swift interface of Dulwich relies only on the pack format
+to store Git objects. Instead of using only an index (pack-sha.idx)
+along with the pack, we add a second file (pack-sha.info). This file
+is automatically created when a client pushes some references on the
+repository. The purpose of this file is to speed up pack creation
+server side when a client fetches some references. Currently this
+.info format is not optimized and may change in future.
index d2d9884ad27ad3bdb0022dfebb6aa7ddd32bb111..385d47ce73a287bc94500f129b4dfa68d7d315a9 100755 (executable)
@@ -38,6 +38,13 @@ from dulwich.pack import Pack, sha_to_hex
 from dulwich.patch import write_tree_diff
 from dulwich.repo import Repo
 
+try:
+    import gevent
+    import geventhttpclient
+    gevent_support = True
+except ImportError:
+    gevent_support = False
+
 
 def cmd_archive(args):
     opts, args = getopt(args, "", [])
@@ -156,6 +163,30 @@ def cmd_init(args):
 
     porcelain.init(path, bare=("--bare" in opts))
 
+def cmd_init_swift(args):
+    if not gevent_support:
+        print "gevent and geventhttpclient libraries are mandatory " \
+              " for use the Swift backend."
+        sys.exit(1)
+    from dulwich.swift import (
+        SwiftRepo,
+        SwiftConnector,
+        load_conf,
+    )
+    opts, args = getopt(args, "c:", [])
+    opts = dict(opts)
+    try:
+        conf = opts['-c']
+        conf = load_conf(conf)
+    except KeyError:
+        conf = load_conf()
+    if args == []:
+        print "Usage: dulwich init-swift [-c config_file] REPONAME"
+        sys.exit(1)
+    else:
+        repo = args[0]
+    scon = SwiftConnector(repo, conf)
+    SwiftRepo.init_bare(scon, conf)
 
 def cmd_clone(args):
     opts, args = getopt(args, "", ["bare"])
@@ -258,6 +289,7 @@ commands = {
     "fetch-pack": cmd_fetch_pack,
     "fetch": cmd_fetch,
     "init": cmd_init,
+    "init-swift": cmd_init_swift,
     "log": cmd_log,
     "reset": cmd_reset,
     "rev-list": cmd_rev_list,
index 099283fdb6bb7d80c65119f0cabc7b9910c5d21a..cfd12810ff1fc6ca19a1fb9a912077c1db6e81d4 100644 (file)
@@ -85,6 +85,12 @@ from dulwich.repo import (
     Repo,
     )
 
+try:
+    import gevent
+    import geventhttpclient
+    gevent_support = True
+except ImportError:
+    gevent_support = False
 
 logger = log_utils.getLogger(__name__)
 
@@ -873,22 +879,44 @@ def main(argv=sys.argv):
     parser = optparse.OptionParser()
     parser.add_option("-b", "--backend", dest="backend",
                       help="Select backend to use.",
-                      choices=["file"], default="file")
+                      choices=["file", "swift"], default="file")
     parser.add_option("-l", "--listen_address", dest="listen_address",
-                      default="localhost",
+                      default="127.0.0.1",
                       help="Binding IP address.")
     parser.add_option("-p", "--port", dest="port", type=int,
                       default=TCP_GIT_PORT,
                       help="Binding TCP port.")
+    parser.add_option("-c", "--swift_config", dest="swift_config",
+                      default="",
+                      help="Path to the configuration file for Swift backend.")
+    parser.add_option("-f", "--fs_path", dest="fs_path",
+                      default=".",
+                      help="Path to GIT repo directory for file backend.")
     options, args = parser.parse_args(argv)
 
     log_utils.default_logging_config()
     if options.backend == "file":
-        if len(argv) > 1:
-            gitdir = args[1]
+        backend = DictBackend({'/': Repo(options.fs_path)})
+    elif options.backend == "swift":
+        if gevent_support:
+            import gevent.monkey
+            gevent.monkey.patch_socket()
+            from dulwich.swift import SwiftRepo
+            from dulwich.swift import load_conf
+            class SwiftSystemBackend(Backend):
+                def __init__(self, logger, conf):
+                    self.conf = conf
+                    self.logger = logger
+
+                def open_repository(self, path):
+                    self.logger.info('opening repository at %s', path)
+                    return SwiftRepo(path, self.conf)
+            conf = load_conf(options.swift_config)
+            backend = SwiftSystemBackend(logger, conf)
         else:
-            gitdir = '.'
-        backend = DictBackend({'/': Repo(gitdir)})
+            print "gevent and geventhttpclient libraries are mandatory " \
+                  " for use the Swift backend."
+            sys.exit(-1)
     else:
         raise Exception("No such backend %s." % backend)
     server = TCPGitServer(backend, options.listen_address,
diff --git a/dulwich/swift.py b/dulwich/swift.py
new file mode 100644 (file)
index 0000000..b4a34ac
--- /dev/null
@@ -0,0 +1,953 @@
+# swift.py -- Repo implementation atop OpenStack SWIFT
+# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
+#
+# Author: Fabien Boucher <fabien.boucher@enovance.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) any later version of
+# the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Repo implementation atop OpenStack SWIFT."""
+
+# TODO: Refactor to share more code with dulwich/repo.py.
+# TODO(fbo): Second attempt to _send() must be notified via real log
+# TODO(fbo): More logs for operations
+
+import os
+import stat
+import zlib
+import tempfile
+import posixpath
+
+from urlparse import urlparse
+from cStringIO import StringIO
+from ConfigParser import ConfigParser
+from geventhttpclient import HTTPClient
+
+from dulwich.repo import (
+    BaseRepo,
+    OBJECTDIR,
+    )
+from dulwich.pack import (
+    PackData,
+    Pack,
+    PackIndexer,
+    PackStreamCopier,
+    write_pack_header,
+    compute_file_sha,
+    iter_sha1,
+    write_pack_index_v2,
+    load_pack_index_file,
+    read_pack_header,
+    _compute_object_size,
+    unpack_object,
+    write_pack_object,
+    )
+from lru_cache import LRUSizeCache
+from dulwich.object_store import (
+    PackBasedObjectStore,
+    PACKDIR,
+    INFODIR,
+    )
+from dulwich.refs import (
+    InfoRefsContainer,
+    read_info_refs,
+    write_info_refs,
+    )
+from dulwich.objects import (
+    Commit,
+    Tree,
+    Tag,
+    S_ISGITLINK,
+    )
+from dulwich.greenthreads import (
+    GreenThreadsMissingObjectFinder,
+    GreenThreadsObjectStoreIterator,
+    )
+
+try:
+    from simplejson import loads as json_loads
+    from simplejson import dumps as json_dumps
+except ImportError:
+    from json import loads as json_loads
+    from json import dumps as json_dumps
+
+
+"""
+# Configuration file sample
+[swift]
+# Authentication URL (Keystone or Swift)
+auth_url = http://127.0.0.1:5000/v2.0
+# Authentication version to use
+auth_ver = 2
+# The tenant and username separated by a semicolon
+username = admin;admin
+# The user password
+password = pass
+# The Object storage region to use (auth v2) (Default RegionOne)
+region_name = RegionOne
+# The Object storage endpoint URL to use (auth v2) (Default internalURL)
+endpoint_type = internalURL
+# Concurrency to use for parallel tasks (Default 10)
+concurrency = 10
+# Size of the HTTP pool (Default 10)
+http_pool_length = 10
+# Timeout delay for HTTP connections (Default 20)
+http_timeout = 20
+# Chunk size to read from pack (Bytes) (Default 12228)
+chunk_length = 12228
+# Cache size (MBytes) (Default 20)
+cache_length = 20
+"""
+
+
+class PackInfoObjectStoreIterator(GreenThreadsObjectStoreIterator):
+    def __len__(self):
+        while len(self.finder.objects_to_send):
+            for _ in xrange(0, len(self.finder.objects_to_send)):
+                sha = self.finder.next()
+                self._shas.append(sha)
+        return len(self._shas)
+
+
+class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder):
+    def next(self):
+        while True:
+            if not self.objects_to_send:
+                return None
+            (sha, name, leaf) = self.objects_to_send.pop()
+            if sha not in self.sha_done:
+                break
+        if not leaf:
+            info = self.object_store.pack_info_get(sha)
+            if info[0] == Commit.type_num:
+                self.add_todo([(info[2], "", False)])
+            elif info[0] == Tree.type_num:
+                self.add_todo([tuple(i) for i in info[1]])
+            elif info[0] == Tag.type_num:
+                self.add_todo([(info[1], None, False)])
+            if sha in self._tagged:
+                self.add_todo([(self._tagged[sha], None, True)])
+        self.sha_done.add(sha)
+        self.progress("counting objects: %d\r" % len(self.sha_done))
+        return (sha, name)
+
+
+def load_conf(path=None, file=None):
+    """Load configuration in global var CONF
+
+    :param path: The path to the configuration file
+    :param file: If provided read instead the file like object
+    """
+    conf = ConfigParser(allow_no_value=True)
+    if file:
+        conf.readfp(file)
+        return conf
+    confpath = None
+    if not path:
+        try:
+            confpath = os.environ['DULWICH_SWIFT_CFG']
+        except KeyError:
+            raise Exception("You need to specify a configuration file")
+    else:
+        confpath = path
+    if not os.path.isfile(confpath):
+        raise Exception("Unable to read configuration file %s" % confpath)
+    conf.read(confpath)
+    return conf
+
+
+def swift_load_pack_index(scon, filename):
+    """Read a pack index file from Swift
+
+    :param scon: a `SwiftConnector` instance
+    :param filename: Path to the index file objectise
+    :return: a `PackIndexer` instance
+    """
+    f = scon.get_object(filename)
+    try:
+        return load_pack_index_file(filename, f)
+    finally:
+        f.close()
+
+
+def pack_info_create(pack_data, pack_index):
+    pack = Pack.from_objects(pack_data, pack_index)
+    info = {}
+    for obj in pack.iterobjects():
+        # Commit
+        if obj.type_num == 1:
+            info[obj.id] = (obj.type_num, obj.parents, obj.tree)
+        # Tree
+        elif obj.type_num == 2:
+            shas = [(s, n, not stat.S_ISDIR(m)) for
+                    n, m, s in obj.iteritems() if not S_ISGITLINK(m)]
+            info[obj.id] = (obj.type_num, shas)
+        # Blob
+        elif obj.type_num == 3:
+            info[obj.id] = None
+        # Tag
+        elif obj.type_num == 4:
+            info[obj.id] = (obj.type_num, obj._object_sha)
+    return zlib.compress(json_dumps(info))
+
+
+def load_pack_info(filename, scon=None, file=None):
+    if not file:
+        f = scon.get_object(filename)
+    else:
+        f = file
+    if not f:
+        return None
+    try:
+        return json_loads(zlib.decompress(f.read()))
+    finally:
+        f.close()
+
+
+class SwiftException(Exception):
+    pass
+
+
+class SwiftConnector(object):
+    """A Connector to swift that manage authentication and errors catching
+    """
+
+    def __init__(self, root, conf):
+        """ Initialize a SwiftConnector
+
+        :param root: The swift container that will act as Git bare repository
+        :param conf: A ConfigParser Object
+        """
+        self.conf = conf
+        self.auth_ver = self.conf.get("swift", "auth_ver")
+        if self.auth_ver not in ["1", "2"]:
+            raise NotImplementedError("Wrong authentication version \
+                    use either 1 or 2")
+        self.auth_url = self.conf.get("swift", "auth_url")
+        self.user = self.conf.get("swift", "username")
+        self.password = self.conf.get("swift", "password")
+        self.concurrency = self.conf.getint('swift', 'concurrency') or 10
+        self.http_timeout = self.conf.getint('swift', 'http_timeout') or 20
+        self.http_pool_length = \
+            self.conf.getint('swift', 'http_pool_length') or 10
+        self.region_name = self.conf.get("swift", "region_name") or "RegionOne"
+        self.endpoint_type = \
+            self.conf.get("swift", "endpoint_type") or "internalURL"
+        self.cache_length = self.conf.getint("swift", "cache_length") or 20
+        self.chunk_length = self.conf.getint("swift", "chunk_length") or 12228
+        self.root = root
+        block_size = 1024 * 12  # 12KB
+        if self.auth_ver == "1":
+            self.storage_url, self.token = self.swift_auth_v1()
+        else:
+            self.storage_url, self.token = self.swift_auth_v2()
+
+        token_header = {'X-Auth-Token': str(self.token)}
+        self.httpclient = \
+            HTTPClient.from_url(str(self.storage_url),
+                                concurrency=self.http_pool_length,
+                                block_size=block_size,
+                                connection_timeout=self.http_timeout,
+                                network_timeout=self.http_timeout,
+                                headers=token_header)
+        self.base_path = str(posixpath.join(urlparse(self.storage_url).path,
+                             self.root))
+
+    def swift_auth_v1(self):
+        self.user = self.user.replace(";", ":")
+        auth_httpclient = HTTPClient.from_url(
+            self.auth_url,
+            connection_timeout=self.http_timeout,
+            network_timeout=self.http_timeout,
+            )
+        headers = {'X-Auth-User': self.user,
+                   'X-Auth-Key': self.password}
+        path = urlparse(self.auth_url).path
+
+        ret = auth_httpclient.request('GET',
+                                      path,
+                                      headers=headers)
+
+        # Should do something with redirections (301 in my case)
+
+        if ret.status_code < 200 or ret.status_code >= 300:
+            raise SwiftException('AUTH v1.0 request failed on ' +
+                                 '%s with error code %s (%s)'
+                                 % (str(auth_httpclient.get_base_url()) +
+                                    path, ret.status_code,
+                                    str(ret.items())))
+        storage_url = ret['X-Storage-Url']
+        token = ret['X-Auth-Token']
+        return storage_url, token
+
+    def swift_auth_v2(self):
+        self.tenant, self.user = self.user.split(';')
+        auth_dict = {}
+        auth_dict['auth'] = {'passwordCredentials':
+                             {
+                                 'username': self.user,
+                                 'password': self.password,
+                             },
+                             'tenantName': self.tenant}
+        auth_json = json_dumps(auth_dict)
+        headers = {'Content-Type': 'application/json'}
+        auth_httpclient = HTTPClient.from_url(
+            self.auth_url,
+            connection_timeout=self.http_timeout,
+            network_timeout=self.http_timeout,
+            )
+        path = urlparse(self.auth_url).path
+        if not path.endswith('tokens'):
+            path = posixpath.join(path, 'tokens')
+        ret = auth_httpclient.request('POST',
+                                      path,
+                                      body=auth_json,
+                                      headers=headers)
+
+        if ret.status_code < 200 or ret.status_code >= 300:
+            raise SwiftException('AUTH v2.0 request failed on ' +
+                                 '%s with error code %s (%s)'
+                                 % (str(auth_httpclient.get_base_url()) +
+                                    path, ret.status_code,
+                                    str(ret.items())))
+        auth_ret_json = json_loads(ret.read())
+        token = auth_ret_json['access']['token']['id']
+        catalogs = auth_ret_json['access']['serviceCatalog']
+        object_store = [o_store for o_store in catalogs if
+                        o_store['type'] == 'object-store'][0]
+        endpoints = object_store['endpoints']
+        endpoint = [endp for endp in endpoints if
+                    endp["region"] == self.region_name][0]
+        return endpoint[self.endpoint_type], token
+
+    def test_root_exists(self):
+        """Check that Swift container exist
+
+        :return: True if exist or None it not
+        """
+        ret = self.httpclient.request('HEAD',
+                                      self.base_path)
+        if ret.status_code == 404:
+            return None
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('HEAD request failed with error code %s'
+                                 % ret.status_code)
+        return True
+
+    def create_root(self):
+        """Create the Swift container
+
+        :raise: `SwiftException` if unable to create
+        """
+        if not self.test_root_exists():
+            ret = self.httpclient.request('PUT',
+                                          self.base_path)
+            if ret.status_code < 200 or ret.status_code > 300:
+                raise SwiftException('PUT request failed with error code %s'
+                                     % ret.status_code)
+
+    def get_container_objects(self):
+        """Retrieve objects list in a container
+
+        :return: A list of dict that describe objects
+                 or None if container does not exist
+        """
+        qs = '?format=json'
+        path = self.base_path + qs
+        ret = self.httpclient.request('GET',
+                                      path)
+        if ret.status_code == 404:
+            return None
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('GET request failed with error code %s'
+                                 % ret.status_code)
+        content = ret.read()
+        return json_loads(content)
+
+    def get_object_stat(self, name):
+        """Retrieve object stat
+
+        :param name: The object name
+        :return: A dict that describe the object
+                 or None if object does not exist
+        """
+        path = self.base_path + '/' + name
+        ret = self.httpclient.request('HEAD',
+                                      path)
+        if ret.status_code == 404:
+            return None
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('HEAD request failed with error code %s'
+                                 % ret.status_code)
+        resp_headers = {}
+        for header, value in ret.iteritems():
+            resp_headers[header.lower()] = value
+        return resp_headers
+
+    def put_object(self, name, content):
+        """Put an object
+
+        :param name: The object name
+        :param content: A file object
+        :raise: `SwiftException` if unable to create
+        """
+        content.seek(0)
+        data = content.read()
+        path = self.base_path + '/' + name
+        headers = {'Content-Length': str(len(data))}
+
+        def _send():
+            ret = self.httpclient.request('PUT',
+                                          path,
+                                          body=data,
+                                          headers=headers)
+            return ret
+
+        try:
+            # Sometime got Broken Pipe - Dirty workaround
+            ret = _send()
+        except Exception:
+            # Second attempt work
+            ret = _send()
+
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('PUT request failed with error code %s'
+                                 % ret.status_code)
+
+    def get_object(self, name, range=None):
+        """Retrieve an object
+
+        :param name: The object name
+        :param range: A string range like "0-10" to
+                      retrieve specified bytes in object content
+        :return: A file like instance
+                 or bytestring if range is specified
+        """
+        headers = {}
+        if range:
+            headers['Range'] = 'bytes=%s' % range
+        path = self.base_path + '/' + name
+        ret = self.httpclient.request('GET',
+                                      path,
+                                      headers=headers)
+        if ret.status_code == 404:
+            return None
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('GET request failed with error code %s'
+                                 % ret.status_code)
+        content = ret.read()
+
+        if range:
+            return content
+        return StringIO(content)
+
+    def del_object(self, name):
+        """Delete an object
+
+        :param name: The object name
+        :raise: `SwiftException` if unable to delete
+        """
+        path = self.base_path + '/' + name
+        ret = self.httpclient.request('DELETE',
+                                      path)
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('DELETE request failed with error code %s'
+                                 % ret.status_code)
+
+    def del_root(self):
+        """Delete the root container by removing container content
+
+        :raise: `SwiftException` if unable to delete
+        """
+        for obj in self.get_container_objects():
+            self.del_object(obj['name'])
+        ret = self.httpclient.request('DELETE',
+                                      self.base_path)
+        if ret.status_code < 200 or ret.status_code > 300:
+            raise SwiftException('DELETE request failed with error code %s'
+                                 % ret.status_code)
+
+
+class SwiftPackReader(object):
+    """A SwiftPackReader that mimic read and sync method
+
+    The reader allows to read a specified amount of bytes from
+    a given offset of a Swift object. A read offset is kept internaly.
+    The reader will read from Swift a specified amount of data to complete
+    its internal buffer. chunk_length specifiy the amount of data
+    to read from Swift.
+    """
+
+    def __init__(self, scon, filename, pack_length):
+        """Initialize a SwiftPackReader
+
+        :param scon: a `SwiftConnector` instance
+        :param filename: the pack filename
+        :param pack_length: The size of the pack object
+        """
+        self.scon = scon
+        self.filename = filename
+        self.pack_length = pack_length
+        self.offset = 0
+        self.base_offset = 0
+        self.buff = ''
+        self.buff_length = self.scon.chunk_length
+
+    def _read(self, more=False):
+        if more:
+            self.buff_length = self.buff_length * 2
+        l = self.base_offset
+        r = min(self.base_offset + self.buff_length,
+                self.pack_length)
+        ret = self.scon.get_object(self.filename,
+                                   range="%s-%s" % (l, r))
+        self.buff = ret
+
+    def read(self, length):
+        """Read a specified amount of Bytes form the pack object
+
+        :param length: amount of bytes to read
+        :return: bytestring
+        """
+        end = self.offset+length
+        if self.base_offset + end > self.pack_length:
+            data = self.buff[self.offset:]
+            self.offset = end
+            return "".join(data)
+        try:
+            self.buff[end]
+        except IndexError:
+            # Need to read more from swift
+            self._read(more=True)
+            return self.read(length)
+        data = self.buff[self.offset:end]
+        self.offset = end
+        return "".join(data)
+
+    def seek(self, offset):
+        """Seek to a specified offset
+
+        :param offset: the offset to seek to
+        """
+        self.base_offset = offset
+        self._read()
+        self.offset = 0
+
+    def read_checksum(self):
+        """Read the checksum from the pack
+
+        :return: the checksum bytestring
+        """
+        return self.scon.get_object(self.filename, range="-20")
+
+
+class SwiftPackData(PackData):
+    """The data contained in a packfile.
+
+    We use the SwiftPackReader to read bytes from packs stored in Swift
+    using the Range header feature of Swift.
+    """
+
+    def __init__(self, scon, filename):
+        """ Initialize a SwiftPackReader
+
+        :param scon: a `SwiftConnector` instance
+        :param filename: the pack filename
+        """
+        self.scon = scon
+        self._filename = filename
+        self._header_size = 12
+        headers = self.scon.get_object_stat(self._filename)
+        self.pack_length = int(headers['content-length'])
+        pack_reader = SwiftPackReader(self.scon, self._filename,
+                                      self.pack_length)
+        (version, self._num_objects) = read_pack_header(pack_reader.read)
+        self._offset_cache = LRUSizeCache(1024*1024*self.scon.cache_length,
+                                          compute_size=_compute_object_size)
+        self.pack = None
+
+    def get_object_at(self, offset):
+        if offset in self._offset_cache:
+            return self._offset_cache[offset]
+        assert isinstance(offset, long) or isinstance(offset, int),\
+            'offset was %r' % offset
+        assert offset >= self._header_size
+        pack_reader = SwiftPackReader(self.scon, self._filename,
+                                      self.pack_length)
+        pack_reader.seek(offset)
+        unpacked, _ = unpack_object(pack_reader.read)
+        return (unpacked.pack_type_num, unpacked._obj())
+
+    def get_stored_checksum(self):
+        pack_reader = SwiftPackReader(self.scon, self._filename,
+                                      self.pack_length)
+        return pack_reader.read_checksum()
+
+    def close(self):
+        pass
+
+
+class SwiftPack(Pack):
+    """A Git pack object.
+
+    Same implementation as pack.Pack except that _idx_load and
+    _data_load are bounded to Swift version of load_pack_index and
+    PackData.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.scon = kwargs['scon']
+        del kwargs['scon']
+        super(SwiftPack, self).__init__(*args, **kwargs)
+        self._pack_info_path = self._basename + '.info'
+        self._pack_info = None
+        self._pack_info_load = lambda: load_pack_info(self._pack_info_path,
+                                                      self.scon)
+        self._idx_load = lambda: swift_load_pack_index(self.scon,
+                                                       self._idx_path)
+        self._data_load = lambda: SwiftPackData(self.scon, self._data_path)
+
+    @property
+    def pack_info(self):
+        """The pack data object being used."""
+        if self._pack_info is None:
+            self._pack_info = self._pack_info_load()
+        return self._pack_info
+
+
+class SwiftObjectStore(PackBasedObjectStore):
+    """A Swift Object Store
+
+    Allow to manage a bare Git repository from Openstack Swift.
+    This object store only supports pack files and not loose objects.
+    """
+    def __init__(self, scon):
+        """Open a Swift object store.
+
+        :param scon: A `SwiftConnector` instance
+        """
+        super(SwiftObjectStore, self).__init__()
+        self.scon = scon
+        self.root = self.scon.root
+        self.pack_dir = posixpath.join(OBJECTDIR, PACKDIR)
+        self._alternates = None
+    
+    @property
+    def packs(self):
+        """List with pack objects."""
+        if not self._pack_cache:
+            self._update_pack_cache()
+        return self._pack_cache.values()
+
+    def _update_pack_cache(self):
+        for pack in self._load_packs():
+            self._pack_cache[pack._basename] = pack
+
+    def _iter_loose_objects(self):
+        """Loose objects are not supported by this repository
+        """
+        return []
+
+    def iter_shas(self, finder):
+        """An iterator over pack's ObjectStore.
+
+        :return: a `ObjectStoreIterator` or `GreenThreadsObjectStoreIterator`
+                 instance if gevent is enabled
+        """
+        shas = iter(finder.next, None)
+        return PackInfoObjectStoreIterator(
+            self, shas, finder, self.scon.concurrency)
+
+    def find_missing_objects(self, *args, **kwargs):
+        kwargs['concurrency'] = self.scon.concurrency
+        return PackInfoMissingObjectFinder(self, *args, **kwargs)
+
+    def _load_packs(self):
+        """Load all packs from Swift
+
+        :return: a list of `SwiftPack` instances
+        """
+        objects = self.scon.get_container_objects()
+        pack_files = [o['name'].replace(".pack", "")
+                      for o in objects if o['name'].endswith(".pack")]
+        return [SwiftPack(pack, scon=self.scon) for pack in pack_files]
+
+    def pack_info_get(self, sha):
+        for pack in self.packs:
+            if sha in pack:
+                return pack.pack_info[sha]
+
+    def _collect_ancestors(self, heads, common=set()):
+        def _find_parents(commit):
+            for pack in self.packs:
+                if commit in pack:
+                    try:
+                        parents = pack.pack_info[commit][1]
+                    except KeyError:
+                        # Seems to have no parents
+                        return []
+                    return parents
+
+        bases = set()
+        commits = set()
+        queue = []
+        queue.extend(heads)
+        while queue:
+            e = queue.pop(0)
+            if e in common:
+                bases.add(e)
+            elif e not in commits:
+                commits.add(e)
+                parents = _find_parents(e)
+                queue.extend(parents)
+        return (commits, bases)
+
+    def add_pack(self):
+        """Add a new pack to this object store.
+
+        :return: Fileobject to write to and a commit function to
+            call when the pack is finished.
+        """
+        f = StringIO()
+
+        def commit():
+            f.seek(0)
+            pack = PackData(file=f, filename="")
+            entries = pack.sorted_entries()
+            if len(entries):
+                basename = posixpath.join(self.pack_dir,
+                                          "pack-%s" %
+                                          iter_sha1(entry[0] for
+                                                    entry in entries))
+                index = StringIO()
+                write_pack_index_v2(index, entries, pack.get_stored_checksum())
+                self.scon.put_object(basename + ".pack", f)
+                f.close()
+                self.scon.put_object(basename + ".idx", index)
+                index.close()
+                final_pack = SwiftPack(basename, scon=self.scon)
+                final_pack.check_length_and_checksum()
+                self._add_known_pack(basename, final_pack)
+                return final_pack
+            else:
+                return None
+
+        def abort():
+            pass
+        return f, commit, abort
+
+    def add_object(self, obj):
+        self.add_objects([(obj, None), ])
+
+    def _pack_cache_stale(self):
+        return False
+
+    def _get_loose_object(self, sha):
+        return None
+
+    def add_thin_pack(self, read_all, read_some):
+        """Read a thin pack
+
+        Read it from a stream and complete it in a temporary file.
+        Then the pack and the corresponding index file are uploaded to Swift.
+        """
+        fd, path = tempfile.mkstemp(prefix='tmp_pack_')
+        f = os.fdopen(fd, 'w+b')
+        try:
+            indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
+            copier = PackStreamCopier(read_all, read_some, f,
+                                      delta_iter=indexer)
+            copier.verify()
+            return self._complete_thin_pack(f, path, copier, indexer)
+        finally:
+            f.close()
+            os.unlink(path)
+
+    def _complete_thin_pack(self, f, path, copier, indexer):
+        entries = list(indexer)
+
+        # Update the header with the new number of objects.
+        f.seek(0)
+        write_pack_header(f, len(entries) + len(indexer.ext_refs()))
+
+        # Must flush before reading (http://bugs.python.org/issue3207)
+        f.flush()
+
+        # Rescan the rest of the pack, computing the SHA with the new header.
+        new_sha = compute_file_sha(f, end_ofs=-20)
+
+        # Must reposition before writing (http://bugs.python.org/issue3207)
+        f.seek(0, os.SEEK_CUR)
+
+        # Complete the pack.
+        for ext_sha in indexer.ext_refs():
+            assert len(ext_sha) == 20
+            type_num, data = self.get_raw(ext_sha)
+            offset = f.tell()
+            crc32 = write_pack_object(f, type_num, data, sha=new_sha)
+            entries.append((ext_sha, offset, crc32))
+        pack_sha = new_sha.digest()
+        f.write(pack_sha)
+        f.flush()
+
+        # Move the pack in.
+        entries.sort()
+        pack_base_name = posixpath.join(
+            self.pack_dir, 'pack-' + iter_sha1(e[0] for e in entries))
+        self.scon.put_object(pack_base_name + '.pack', f)
+
+        # Write the index.
+        filename = pack_base_name + '.idx'
+        index_file = StringIO()
+        write_pack_index_v2(index_file, entries, pack_sha)
+        self.scon.put_object(filename, index_file)
+
+        # Write pack info.
+        f.seek(0)
+        pack_data = PackData(filename="", file=f)
+        index_file.seek(0)
+        pack_index = load_pack_index_file('', index_file)
+        serialized_pack_info = pack_info_create(pack_data, pack_index)
+        f.close()
+        index_file.close()
+        pack_info_file = StringIO(serialized_pack_info)
+        filename = pack_base_name + '.info'
+        self.scon.put_object(filename, pack_info_file)
+        pack_info_file.close()
+
+        # Add the pack to the store and return it.
+        final_pack = SwiftPack(pack_base_name, scon=self.scon)
+        final_pack.check_length_and_checksum()
+        self._add_known_pack(pack_base_name, final_pack)
+        return final_pack
+
+
+class SwiftInfoRefsContainer(InfoRefsContainer):
+    """Manage references in info/refs object.
+    """
+
+    def __init__(self, scon, store):
+        self.scon = scon
+        self.filename = 'info/refs'
+        self.store = store
+        f = self.scon.get_object(self.filename)
+        if not f:
+            f = StringIO('')
+        super(SwiftInfoRefsContainer, self).__init__(f)
+
+    def _load_check_ref(self, name, old_ref):
+        self._check_refname(name)
+        f = self.scon.get_object(self.filename)
+        if not f:
+            return {}
+        refs = read_info_refs(f)
+        if old_ref is not None:
+            if refs[name] != old_ref:
+                return False
+        return refs
+
+    def _write_refs(self, refs):
+        f = StringIO()
+        f.writelines(write_info_refs(refs, self.store))
+        self.scon.put_object(self.filename, f)
+
+    def set_if_equals(self, name, old_ref, new_ref):
+        """Set a refname to new_ref only if it currently equals old_ref.
+        """
+        if name == 'HEAD':
+            return True
+        refs = self._load_check_ref(name, old_ref)
+        if not isinstance(refs, dict):
+            return False
+        refs[name] = new_ref
+        self._write_refs(refs)
+        self._refs[name] = new_ref
+        return True
+
+    def remove_if_equals(self, name, old_ref):
+        """Remove a refname only if it currently equals old_ref.
+        """
+        if name == 'HEAD':
+            return True
+        refs = self._load_check_ref(name, old_ref)
+        if not isinstance(refs, dict):
+            return False
+        del refs[name]
+        self._write_refs(refs)
+        del self._refs[name]
+        return True
+    
+    def allkeys(self):
+        try:
+            self._refs['HEAD'] = self._refs['refs/heads/master']
+        except KeyError:
+            pass
+        return self._refs.keys()
+
+
+class SwiftRepo(BaseRepo):
+
+    def __init__(self, root, conf):
+        """Init a Git bare Repository on top of a Swift container.
+
+        References are managed in info/refs objects by
+        `SwiftInfoRefsContainer`. The root attribute is the Swift
+        container that contain the Git bare repository.
+
+        :param root: The container which contains the bare repo
+        :param conf: A ConfigParser object
+        """
+        self.root = root.lstrip('/')
+        self.conf = conf
+        self.scon = SwiftConnector(self.root, self.conf)
+        objects = self.scon.get_container_objects()
+        if not objects:
+            raise Exception('There is not any GIT repo here : %s' % self.root)
+        objects = [o['name'].split('/')[0] for o in objects]
+        if OBJECTDIR not in objects:
+            raise Exception('This repository (%s) is not bare.' % self.root)
+        self.bare = True
+        self._controldir = self.root
+        object_store = SwiftObjectStore(self.scon)
+        refs = SwiftInfoRefsContainer(self.scon, object_store)
+        BaseRepo.__init__(self, object_store, refs)
+
+    def _put_named_file(self, filename, contents):
+        """Put an object in a Swift container
+
+        :param filename: the path to the object to put on Swift
+        :param contents: the content as bytestring
+        """
+        f = StringIO()
+        f.write(contents)
+        self.scon.put_object(filename, f)
+        f.close()
+
+    @classmethod
+    def init_bare(cls, scon, conf):
+        """Create a new bare repository.
+
+        :param scon: a `SwiftConnector` instance
+        :param conf: a ConfigParser object
+        :return: a `SwiftRepo` instance
+        """
+        scon.create_root()
+        for obj in [posixpath.join(OBJECTDIR, PACKDIR),
+                    posixpath.join(INFODIR, 'refs')]:
+            scon.put_object(obj, StringIO(''))
+        ret = cls(scon.root, conf)
+        ret._init_files(True)
+        return ret
index 06f0b7b743cf752537a4e6bbc5be0adb145bd04d..9d3b2a6f3ad9a0b82f2207674dc173133c516700 100644 (file)
@@ -131,6 +131,7 @@ def self_test_suite():
         'server',
         'walk',
         'web',
+        'swift',
         ]
     module_names = ['dulwich.tests.test_' + name for name in names]
     loader = unittest.TestLoader()
diff --git a/dulwich/tests/test_swift.py b/dulwich/tests/test_swift.py
new file mode 100644 (file)
index 0000000..b7b7cf3
--- /dev/null
@@ -0,0 +1,631 @@
+# test_swift.py -- Unittests for the Swift backend.
+# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
+#
+# Author: Fabien Boucher <fabien.boucher@enovance.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) any later version of
+# the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Tests for dulwich.swift."""
+
+import posixpath
+
+from time import time
+from cStringIO import StringIO
+from contextlib import nested
+
+from dulwich.tests import (
+    TestCase,
+    skipIf,
+    )
+from dulwich.tests.test_object_store import (
+    ObjectStoreTests,
+    )
+from dulwich.tests.utils import (
+    build_pack,
+    )
+from dulwich.objects import (
+    Blob,
+    Commit,
+    Tree,
+    Tag,
+    parse_timezone,
+    )
+from dulwich.pack import (
+    REF_DELTA,
+    write_pack_index_v2,
+    PackData,
+    load_pack_index_file,
+    )
+
+try:
+    from simplejson import dumps as json_dumps
+except ImportError:
+    from json import dumps as json_dumps
+
+try:
+    import gevent
+    import geventhttpclient
+    from mock import patch
+    lib_support = True
+    from dulwich import swift
+except ImportError:
+    lib_support = False
+
+skipmsg = "Required libraries are not installed (gevent, geventhttpclient, mock)"
+
+config_file = """[swift]
+auth_url = http://127.0.0.1:8080/auth/%(version_str)s
+auth_ver = %(version_int)s
+username = test;tester
+password = testing
+region_name = %(region_name)s
+endpoint_type = %(endpoint_type)s
+concurrency = %(concurrency)s
+chunk_length = %(chunk_length)s
+cache_length = %(cache_length)s
+http_pool_length = %(http_pool_length)s
+http_timeout = %(http_timeout)s
+"""
+
+def_config_file = {'version_str': 'v1.0',
+                   'version_int': 1,
+                   'concurrency': 1,
+                   'chunk_length': 12228,
+                   'cache_length': 1,
+                   'region_name': 'test',
+                   'endpoint_type': 'internalURL',
+                   'http_pool_length': 1,
+                   'http_timeout': 1}
+
+
+def create_swift_connector(store={}):
+    return lambda root, conf: FakeSwiftConnector(root,
+                                                 conf=conf,
+                                                 store=store)
+
+
+class Response(object):
+    def __init__(self, headers={}, status=200, content=None):
+        self.headers = headers
+        self.status_code = status
+        self.content = content
+
+    def __getitem__(self, key):
+        return self.headers[key]
+
+    def items(self):
+        return self.headers
+
+    def iteritems(self):
+        for k, v in self.headers.iteritems():
+            yield k, v
+
+    def read(self):
+        return self.content
+
+
+def fake_auth_request_v1(*args, **kwargs):
+    ret = Response({'X-Storage-Url':
+                    'http://127.0.0.1:8080/v1.0/AUTH_fakeuser',
+                    'X-Auth-Token': '12' * 10},
+                   200)
+    return ret
+
+def fake_auth_request_v1_error(*args, **kwargs):
+    ret = Response({},
+                   401)
+    return ret
+
+
+def fake_auth_request_v2(*args, **kwargs):
+    s_url = 'http://127.0.0.1:8080/v1.0/AUTH_fakeuser'
+    resp = {'access': {'token': {'id': '12' * 10},
+                       'serviceCatalog':
+                       [
+                           {'type': 'object-store',
+                            'endpoints': [{'region': 'test',
+                                          'internalURL': s_url,
+                                           },
+                                          ]
+                            },
+                       ]
+                       }
+            }
+    ret = Response(status=200, content=json_dumps(resp))
+    return ret
+
+
+def create_commit(data, marker='Default', blob=None):
+    if not blob:
+        blob = Blob.from_string('The blob content %s' % marker)
+    tree = Tree()
+    tree.add("thefile_%s" % marker, 0100644, blob.id)
+    cmt = Commit()
+    if data:
+        assert isinstance(data[-1], Commit)
+        cmt.parents = [data[-1].id]
+    cmt.tree = tree.id
+    author = "John Doe %s <john@doe.net>" % marker
+    cmt.author = cmt.committer = author
+    tz = parse_timezone('-0200')[0]
+    cmt.commit_time = cmt.author_time = int(time())
+    cmt.commit_timezone = cmt.author_timezone = tz
+    cmt.encoding = "UTF-8"
+    cmt.message = "The commit message %s" % marker
+    tag = Tag()
+    tag.tagger = "john@doe.net"
+    tag.message = "Annotated tag"
+    tag.tag_timezone = parse_timezone('-0200')[0]
+    tag.tag_time = cmt.author_time
+    tag.object = (Commit, cmt.id)
+    tag.name = "v_%s_0.1" % marker
+    return blob, tree, tag, cmt
+
+
+def create_commits(length=1, marker='Default'):
+    data = []
+    for i in xrange(0, length):
+        _marker = "%s_%s" % (marker, i)
+        blob, tree, tag, cmt = create_commit(data, _marker)
+        data.extend([blob, tree, tag, cmt])
+    return data
+
+@skipIf(not lib_support, skipmsg)
+class FakeSwiftConnector(object):
+
+    def __init__(self, root, conf, store=None):
+        if store:
+            self.store = store
+        else:
+            self.store = {}
+        self.conf = conf
+        self.root = root
+        self.concurrency = 1
+        self.chunk_length = 12228
+        self.cache_length = 1
+
+    def put_object(self, name, content):
+        name = posixpath.join(self.root, name)
+        if hasattr(content, 'seek'):
+            content.seek(0)
+            content = content.read()
+        self.store[name] = content
+
+    def get_object(self, name, range=None):
+        name = posixpath.join(self.root, name)
+        if not range:
+            try:
+                return StringIO(self.store[name])
+            except KeyError:
+                return None
+        else:
+            l, r = range.split('-')
+            try:
+                if not l:
+                    r = -int(r)
+                    return self.store[name][r:]
+                else:
+                    return self.store[name][int(l):int(r)]
+            except KeyError:
+                return None
+
+    def get_container_objects(self):
+        return [{'name': k.replace(self.root + '/', '')}
+                for k in self.store]
+
+    def create_root(self):
+        if self.root in self.store.keys():
+            pass
+        else:
+            self.store[self.root] = ''
+
+    def get_object_stat(self, name):
+        name = posixpath.join(self.root, name)
+        if not name in self.store:
+            return None
+        return {'content-length': len(self.store[name])}
+
+
+@skipIf(not lib_support, skipmsg)
+class TestSwiftObjectStore(TestCase):
+
+    def setUp(self):
+        super(TestSwiftObjectStore, self).setUp()
+        self.conf = swift.load_conf(file=StringIO(config_file %
+                                                  def_config_file))
+        self.fsc = FakeSwiftConnector('fakerepo', conf=self.conf)
+
+    def _put_pack(self, sos, commit_amount=1, marker='Default'):
+        odata = create_commits(length=commit_amount, marker=marker)
+        data = [(d.type_num, d.as_raw_string()) for d in odata]
+        f = StringIO()
+        build_pack(f, data, store=sos)
+        sos.add_thin_pack(f.read, None)
+        return odata
+
+    def test_load_packs(self):
+        store = {'fakerepo/objects/pack/pack-'+'1'*40+'.idx': '',
+                 'fakerepo/objects/pack/pack-'+'1'*40+'.pack': '',
+                 'fakerepo/objects/pack/pack-'+'1'*40+'.info': '',
+                 'fakerepo/objects/pack/pack-'+'2'*40+'.idx': '',
+                 'fakerepo/objects/pack/pack-'+'2'*40+'.pack': '',
+                 'fakerepo/objects/pack/pack-'+'2'*40+'.info': ''}
+        fsc = FakeSwiftConnector('fakerepo', conf=self.conf, store=store)
+        sos = swift.SwiftObjectStore(fsc)
+        packs = sos._load_packs()
+        self.assertEqual(len(packs), 2)
+        for pack in packs:
+            self.assertTrue(isinstance(pack, swift.SwiftPack))
+
+    def test_add_thin_pack(self):
+        sos = swift.SwiftObjectStore(self.fsc)
+        self._put_pack(sos, 1, 'Default')
+        self.assertEqual(len(self.fsc.store), 3)
+
+    def test_find_missing_objects(self):
+        commit_amount = 3
+        sos = swift.SwiftObjectStore(self.fsc)
+        odata = self._put_pack(sos, commit_amount, 'Default')
+        head = odata[-1].id
+        i = sos.iter_shas(sos.find_missing_objects([],
+                                                   [head, ],
+                                                   progress=None,
+                                                   get_tagged=None))
+        self.assertEqual(len(i), commit_amount * 3)
+        shas = [d.id for d in odata]
+        for sha, path in i:
+            self.assertIn(sha.id, shas)
+
+    def test_find_missing_objects_with_tag(self):
+        commit_amount = 3
+        sos = swift.SwiftObjectStore(self.fsc)
+        odata = self._put_pack(sos, commit_amount, 'Default')
+        head = odata[-1].id
+        peeled_sha = dict([(sha.object[1], sha.id)
+                           for sha in odata if isinstance(sha, Tag)])
+        get_tagged = lambda: peeled_sha
+        i = sos.iter_shas(sos.find_missing_objects([],
+                                                   [head, ],
+                                                   progress=None,
+                                                   get_tagged=get_tagged))
+        self.assertEqual(len(i), commit_amount * 4)
+        shas = [d.id for d in odata]
+        for sha, path in i:
+            self.assertIn(sha.id, shas)
+
+    def test_find_missing_objects_with_common(self):
+        commit_amount = 3
+        sos = swift.SwiftObjectStore(self.fsc)
+        odata = self._put_pack(sos, commit_amount, 'Default')
+        head = odata[-1].id
+        have = odata[7].id
+        i = sos.iter_shas(sos.find_missing_objects([have, ],
+                                                   [head, ],
+                                                   progress=None,
+                                                   get_tagged=None))
+        self.assertEqual(len(i), 3)
+
+    def test_find_missing_objects_multiple_packs(self):
+        sos = swift.SwiftObjectStore(self.fsc)
+        commit_amount_a = 3
+        odataa = self._put_pack(sos, commit_amount_a, 'Default1')
+        heada = odataa[-1].id
+        commit_amount_b = 2
+        odatab = self._put_pack(sos, commit_amount_b, 'Default2')
+        headb = odatab[-1].id
+        i = sos.iter_shas(sos.find_missing_objects([],
+                                                   [heada, headb],
+                                                   progress=None,
+                                                   get_tagged=None))
+        self.assertEqual(len(self.fsc.store), 6)
+        self.assertEqual(len(i),
+                         commit_amount_a * 3 +
+                         commit_amount_b * 3)
+        shas = [d.id for d in odataa]
+        shas.extend([d.id for d in odatab])
+        for sha, path in i:
+            self.assertIn(sha.id, shas)
+
+    def test_add_thin_pack_ext_ref(self):
+        sos = swift.SwiftObjectStore(self.fsc)
+        odata = self._put_pack(sos, 1, 'Default1')
+        ref_blob_content = odata[0].as_raw_string()
+        ref_blob_id = odata[0].id
+        new_blob = Blob.from_string(ref_blob_content.replace('blob',
+                                                             'yummy blob'))
+        blob, tree, tag, cmt = \
+            create_commit([], marker='Default2', blob=new_blob)
+        data = [(REF_DELTA, (ref_blob_id, blob.as_raw_string())),
+                (tree.type_num, tree.as_raw_string()),
+                (cmt.type_num, cmt.as_raw_string()),
+                (tag.type_num, tag.as_raw_string())]
+        f = StringIO()
+        build_pack(f, data, store=sos)
+        sos.add_thin_pack(f.read, None)
+        self.assertEqual(len(self.fsc.store), 6)
+
+
+@skipIf(not lib_support, skipmsg)
+class TestSwiftRepo(TestCase):
+
+    def setUp(self):
+        super(TestSwiftRepo, self).setUp()
+        self.conf = swift.load_conf(file=StringIO(config_file %
+                                                  def_config_file))
+
+    def test_init(self):
+        store = {'fakerepo/objects/pack': ''}
+        with patch('dulwich.swift.SwiftConnector',
+                   new_callable=create_swift_connector,
+                   store=store):
+            swift.SwiftRepo('fakerepo', conf=self.conf)
+
+    def test_init_no_data(self):
+        with patch('dulwich.swift.SwiftConnector',
+                   new_callable=create_swift_connector):
+            self.assertRaises(Exception, swift.SwiftRepo,
+                              'fakerepo', self.conf)
+
+    def test_init_bad_data(self):
+        store = {'fakerepo/.git/objects/pack': ''}
+        with patch('dulwich.swift.SwiftConnector',
+                   new_callable=create_swift_connector,
+                   store=store):
+            self.assertRaises(Exception, swift.SwiftRepo,
+                              'fakerepo', self.conf)
+
+    def test_put_named_file(self):
+        store = {'fakerepo/objects/pack': ''}
+        with patch('dulwich.swift.SwiftConnector',
+                   new_callable=create_swift_connector,
+                   store=store):
+            repo = swift.SwiftRepo('fakerepo', conf=self.conf)
+            desc = 'Fake repo'
+            repo._put_named_file('description', desc)
+        self.assertEqual(repo.scon.store['fakerepo/description'],
+                         desc)
+
+    def test_init_bare(self):
+        fsc = FakeSwiftConnector('fakeroot', conf=self.conf)
+        with patch('dulwich.swift.SwiftConnector',
+                   new_callable=create_swift_connector,
+                   store=fsc.store):
+            swift.SwiftRepo.init_bare(fsc, conf=self.conf)
+        self.assertIn('fakeroot/objects/pack', fsc.store)
+        self.assertIn('fakeroot/info/refs', fsc.store)
+        self.assertIn('fakeroot/description', fsc.store)
+
+
+@skipIf(not lib_support, skipmsg)
+class TestPackInfoLoadDump(TestCase):
+    def setUp(self):
+        conf = swift.load_conf(file=StringIO(config_file %
+                                             def_config_file))
+        sos = swift.SwiftObjectStore(
+            FakeSwiftConnector('fakerepo', conf=conf))
+        commit_amount = 10
+        self.commits = create_commits(length=commit_amount, marker="m")
+        data = [(d.type_num, d.as_raw_string()) for d in self.commits]
+        f = StringIO()
+        fi = StringIO()
+        expected = build_pack(f, data, store=sos)
+        entries = [(sha, ofs, checksum) for
+                   ofs, _, _, sha, checksum in expected]
+        self.pack_data = PackData.from_file(file=f, size=None)
+        write_pack_index_v2(
+            fi, entries, self.pack_data.calculate_checksum())
+        fi.seek(0)
+        self.pack_index = load_pack_index_file('', fi)
+
+#    def test_pack_info_perf(self):
+#        dump_time = []
+#        load_time = []
+#        for i in xrange(0, 100):
+#            start = time()
+#            dumps = swift.pack_info_create(self.pack_data, self.pack_index)
+#            dump_time.append(time() - start)
+#        for i in xrange(0, 100):
+#            start = time()
+#            pack_infos = swift.load_pack_info('', file=StringIO(dumps))
+#            load_time.append(time() - start)
+#        print sum(dump_time) / float(len(dump_time))
+#        print sum(load_time) / float(len(load_time))
+
+    def test_pack_info(self):
+        dumps = swift.pack_info_create(self.pack_data, self.pack_index)
+        pack_infos = swift.load_pack_info('', file=StringIO(dumps))
+        for obj in self.commits:
+            self.assertIn(obj.id, pack_infos)
+
+
+@skipIf(not lib_support, skipmsg)
+class TestSwiftInfoRefsContainer(TestCase):
+
+    def setUp(self):
+        super(TestSwiftInfoRefsContainer, self).setUp()
+        content = \
+            "22effb216e3a82f97da599b8885a6cadb488b4c5\trefs/heads/master\n" + \
+            "cca703b0e1399008b53a1a236d6b4584737649e4\trefs/heads/dev"
+        self.store = {'fakerepo/info/refs': content}
+        self.conf = swift.load_conf(file=StringIO(config_file %
+                                                  def_config_file))
+        self.fsc = FakeSwiftConnector('fakerepo', conf=self.conf)
+        self.object_store = {}
+
+    def test_init(self):
+        """info/refs does not exists"""
+        irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
+        self.assertEqual(len(irc._refs), 0)
+        self.fsc.store = self.store
+        irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
+        self.assertIn('refs/heads/dev', irc.allkeys())
+        self.assertIn('refs/heads/master', irc.allkeys())
+
+    def test_set_if_equals(self):
+        self.fsc.store = self.store
+        irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
+        irc.set_if_equals('refs/heads/dev',
+                          "cca703b0e1399008b53a1a236d6b4584737649e4", '1'*40)
+        self.assertEqual(irc['refs/heads/dev'], '1'*40)
+
+    def test_remove_if_equals(self):
+        self.fsc.store = self.store
+        irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
+        irc.remove_if_equals('refs/heads/dev',
+                             "cca703b0e1399008b53a1a236d6b4584737649e4")
+        self.assertNotIn('refs/heads/dev', irc.allkeys())
+
+
+@skipIf(not lib_support, skipmsg)
+class TestSwiftConnector(TestCase):
+
+    def setUp(self):
+        super(TestSwiftConnector, self).setUp()
+        self.conf = swift.load_conf(file=StringIO(config_file %
+                                                  def_config_file))
+        with patch('geventhttpclient.HTTPClient.request',
+                   fake_auth_request_v1):
+            self.conn = swift.SwiftConnector('fakerepo', conf=self.conf)
+
+    def test_init_connector(self):
+        self.assertEqual(self.conn.auth_ver, '1')
+        self.assertEqual(self.conn.auth_url,
+                         'http://127.0.0.1:8080/auth/v1.0')
+        self.assertEqual(self.conn.user, 'test:tester')
+        self.assertEqual(self.conn.password, 'testing')
+        self.assertEqual(self.conn.root, 'fakerepo')
+        self.assertEqual(self.conn.storage_url,
+                         'http://127.0.0.1:8080/v1.0/AUTH_fakeuser')
+        self.assertEqual(self.conn.token, '12' * 10)
+        self.assertEqual(self.conn.http_timeout, 1)
+        self.assertEqual(self.conn.http_pool_length, 1)
+        self.assertEqual(self.conn.concurrency, 1)
+        self.conf.set('swift', 'auth_ver', '2')
+        self.conf.set('swift', 'auth_url', 'http://127.0.0.1:8080/auth/v2.0')
+        with patch('geventhttpclient.HTTPClient.request',
+                   fake_auth_request_v2):
+            conn = swift.SwiftConnector('fakerepo', conf=self.conf)
+        self.assertEqual(conn.user, 'tester')
+        self.assertEqual(conn.tenant, 'test')
+        self.conf.set('swift', 'auth_ver', '1')
+        self.conf.set('swift', 'auth_url', 'http://127.0.0.1:8080/auth/v1.0')
+        with patch('geventhttpclient.HTTPClient.request',
+                   fake_auth_request_v1_error):
+            self.assertRaises(swift.SwiftException,
+                              lambda: swift.SwiftConnector('fakerepo',
+                                                           conf=self.conf))
+
+    def test_root_exists(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response()):
+            self.assertEqual(self.conn.test_root_exists(), True)
+
+    def test_root_not_exists(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response(status=404)):
+            self.assertEqual(self.conn.test_root_exists(), None)
+
+    def test_create_root(self):
+        ctx = [patch('dulwich.swift.SwiftConnector.test_root_exists',
+                     lambda *args: None),
+               patch('geventhttpclient.HTTPClient.request',
+                     lambda *args: Response())]
+        with nested(*ctx):
+            self.assertEqual(self.conn.create_root(), None)
+
+    def test_create_root_fails(self):
+        ctx = [patch('dulwich.swift.SwiftConnector.test_root_exists',
+                     lambda *args: None),
+               patch('geventhttpclient.HTTPClient.request',
+                     lambda *args: Response(status=404))]
+        with nested(*ctx):
+            self.assertRaises(swift.SwiftException,
+                              lambda: self.conn.create_root())
+
+    def test_get_container_objects(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response(content=json_dumps(
+                       (({'name': 'a'}, {'name': 'b'}))))):
+            self.assertEqual(len(self.conn.get_container_objects()), 2)
+
+    def test_get_container_objects_fails(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response(status=404)):
+            self.assertEqual(self.conn.get_container_objects(), None)
+
+    def test_get_object_stat(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response(headers={'content-length': '10'})):
+            self.assertEqual(self.conn.get_object_stat('a')['content-length'],
+                             '10')
+
+    def test_get_object_stat_fails(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response(status=404)):
+            self.assertEqual(self.conn.get_object_stat('a'), None)
+
+    def test_put_object(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args, **kwargs: Response()):
+            self.assertEqual(self.conn.put_object('a', StringIO('content')),
+                             None)
+
+    def test_put_object_fails(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args, **kwargs: Response(status=400)):
+            self.assertRaises(swift.SwiftException,
+                              lambda: self.conn.put_object(
+                                  'a', StringIO('content')))
+
+    def test_get_object(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args, **kwargs: Response(content='content')):
+            self.assertEqual(self.conn.get_object('a').read(), 'content')
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args, **kwargs: Response(content='content')):
+            self.assertEqual(self.conn.get_object('a', range='0-6'), 'content')
+
+    def test_get_object_fails(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args, **kwargs: Response(status=404)):
+            self.assertEqual(self.conn.get_object('a'), None)
+
+    def test_del_object(self):
+        with patch('geventhttpclient.HTTPClient.request',
+                   lambda *args: Response()):
+            self.assertEqual(self.conn.del_object('a'), None)
+
+    def test_del_root(self):
+        ctx = [patch('dulwich.swift.SwiftConnector.del_object',
+                     lambda *args: None),
+               patch('dulwich.swift.SwiftConnector.get_container_objects',
+                     lambda *args: ({'name': 'a'}, {'name': 'b'})),
+               patch('geventhttpclient.HTTPClient.request',
+                     lambda *args: Response())]
+        with nested(*ctx):
+            self.assertEqual(self.conn.del_root(), None)
+
+
+@skipIf(not lib_support, skipmsg)
+class SwiftObjectStoreTests(ObjectStoreTests, TestCase):
+
+    def setUp(self):
+        TestCase.setUp(self)
+        conf = swift.load_conf(file=StringIO(config_file %
+                               def_config_file))
+        fsc = FakeSwiftConnector('fakerepo', conf=conf)
+        self.store = swift.SwiftObjectStore(fsc)
diff --git a/dulwich/tests_swift/__init__.py b/dulwich/tests_swift/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dulwich/tests_swift/test_smoke.py b/dulwich/tests_swift/test_smoke.py
new file mode 100644 (file)
index 0000000..2610709
--- /dev/null
@@ -0,0 +1,315 @@
+# test_smoke.py -- Functional tests for the Swift backend.
+# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
+#
+# Author: Fabien Boucher <fabien.boucher@enovance.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) any later version of
+# the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+import os
+import unittest
+import tempfile
+import shutil
+
+import gevent
+from gevent import monkey
+monkey.patch_all()
+
+from dulwich import server
+from dulwich import swift
+from dulwich import repo
+from dulwich import index
+from dulwich import client
+from dulwich import objects
+
+
+"""Start functional tests
+
+A Swift installation must be available before
+starting those tests. The account and authentication method used
+during this functional tests must be changed in the configuration file
+passed as environment variable.
+The container used to create a fake repository is defined
+in cls.fakerepo and will be deleted after the tests.
+
+DULWICH_SWIFT_CFG=/tmp/conf.cfg PYTHONPATH=. python -m unittest \
+    dulwich.tests_swift.test_smoke
+"""
+
+
+class DulwichServer():
+    """Start the TCPGitServer with Swift backend
+    """
+    def __init__(self, backend, port):
+        self.port = port
+        self.backend = backend
+
+    def run(self):
+        self.server = server.TCPGitServer(self.backend,
+                                          'localhost',
+                                          port=self.port)
+        self.job = gevent.spawn(self.server.serve_forever)
+
+    def stop(self):
+        self.server.shutdown()
+        gevent.joinall((self.job,))
+
+
+class SwiftSystemBackend(server.Backend):
+
+    def open_repository(self, path):
+        return swift.SwiftRepo(path, conf=swift.load_conf())
+
+
+class SwiftRepoSmokeTest(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.backend = SwiftSystemBackend()
+        cls.port = 9418
+        cls.server_address = 'localhost'
+        cls.fakerepo = 'fakerepo'
+        cls.th_server = DulwichServer(cls.backend, cls.port)
+        cls.th_server.run()
+        cls.conf = swift.load_conf()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.th_server.stop()
+
+    def setUp(self):
+        self.scon = swift.SwiftConnector(self.fakerepo, self.conf)
+        if self.scon.test_root_exists():
+            try:
+                self.scon.del_root()
+            except swift.SwiftException:
+                pass
+        self.temp_d = tempfile.mkdtemp()
+        if os.path.isdir(self.temp_d):
+            shutil.rmtree(self.temp_d)
+
+    def tearDown(self):
+        if self.scon.test_root_exists():
+            try:
+                self.scon.del_root()
+            except swift.SwiftException:
+                pass
+        if os.path.isdir(self.temp_d):
+            shutil.rmtree(self.temp_d)
+
+    def test_init_bare(self):
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        self.assertTrue(self.scon.test_root_exists())
+        obj = self.scon.get_container_objects()
+        filtered = [o for o in obj if o['name'] == 'info/refs'
+                    or o['name'] == 'objects/pack']
+        self.assertEqual(len(filtered), 2)
+
+    def test_clone_bare(self):
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        remote_refs = tcp_client.fetch(self.fakerepo, local_repo)
+        # The remote repo is empty (no refs retreived)
+        self.assertEqual(remote_refs, None)
+
+    def test_push_commit(self):
+        def determine_wants(*args):
+            return {"refs/heads/master": local_repo.refs["HEAD"]}
+
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        # Nothing in the staging area
+        local_repo.do_commit('Test commit', 'fbo@localhost')
+        sha = local_repo.refs.read_loose_ref('refs/heads/master')
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        tcp_client.send_pack(self.fakerepo,
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+        swift_repo = swift.SwiftRepo("fakerepo", self.conf)
+        remote_sha = swift_repo.refs.read_loose_ref('refs/heads/master')
+        self.assertEqual(sha, remote_sha)
+
+    def test_push_branch(self):
+        def determine_wants(*args):
+            return {"refs/heads/mybranch":
+                    local_repo.refs["refs/heads/mybranch"]}
+
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        # Nothing in the staging area
+        local_repo.do_commit('Test commit', 'fbo@localhost',
+                             ref='refs/heads/mybranch')
+        sha = local_repo.refs.read_loose_ref('refs/heads/mybranch')
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        tcp_client.send_pack("/fakerepo",
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+        swift_repo = swift.SwiftRepo(self.fakerepo, self.conf)
+        remote_sha = swift_repo.refs.read_loose_ref('refs/heads/mybranch')
+        self.assertEqual(sha, remote_sha)
+
+    def test_push_multiple_branch(self):
+        def determine_wants(*args):
+            return {"refs/heads/mybranch":
+                    local_repo.refs["refs/heads/mybranch"],
+                    "refs/heads/master":
+                    local_repo.refs["refs/heads/master"],
+                    "refs/heads/pullr-108":
+                    local_repo.refs["refs/heads/pullr-108"]}
+
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        # Nothing in the staging area
+        local_shas = {}
+        remote_shas = {}
+        for branch in ('master', 'mybranch', 'pullr-108'):
+            local_shas[branch] = local_repo.do_commit(
+                'Test commit %s' % branch, 'fbo@localhost',
+                ref='refs/heads/%s' % branch)
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        tcp_client.send_pack(self.fakerepo,
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+        swift_repo = swift.SwiftRepo("fakerepo", self.conf)
+        for branch in ('master', 'mybranch', 'pullr-108'):
+            remote_shas[branch] = swift_repo.refs.read_loose_ref(
+                'refs/heads/%s' % branch)
+        self.assertDictEqual(local_shas, remote_shas)
+
+    def test_push_data_branch(self):
+        def determine_wants(*args):
+            return {"refs/heads/master": local_repo.refs["HEAD"]}
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        os.mkdir(os.path.join(self.temp_d, "dir"))
+        files = ('testfile', 'testfile2', 'dir/testfile3')
+        i = 0
+        for f in files:
+            file(os.path.join(self.temp_d, f), 'w').write("DATA %s" % i)
+            i += 1
+        local_repo.stage(files)
+        local_repo.do_commit('Test commit', 'fbo@localhost',
+                             ref='refs/heads/master')
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        tcp_client.send_pack(self.fakerepo,
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+        swift_repo = swift.SwiftRepo("fakerepo", self.conf)
+        commit_sha = swift_repo.refs.read_loose_ref('refs/heads/master')
+        otype, data = swift_repo.object_store.get_raw(commit_sha)
+        commit = objects.ShaFile.from_raw_string(otype, data)
+        otype, data = swift_repo.object_store.get_raw(commit._tree)
+        tree = objects.ShaFile.from_raw_string(otype, data)
+        objs = tree.items()
+        objs_ = []
+        for tree_entry in objs:
+            objs_.append(swift_repo.object_store.get_raw(tree_entry.sha))
+        # Blob
+        self.assertEqual(objs_[1][1], 'DATA 0')
+        self.assertEqual(objs_[2][1], 'DATA 1')
+        # Tree
+        self.assertEqual(objs_[0][0], 2)
+
+    def test_clone_then_push_data(self):
+        self.test_push_data_branch()
+        shutil.rmtree(self.temp_d)
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        remote_refs = tcp_client.fetch(self.fakerepo, local_repo)
+        files = (os.path.join(self.temp_d, 'testfile'),
+                 os.path.join(self.temp_d, 'testfile2'))
+        local_repo["HEAD"] = remote_refs["refs/heads/master"]
+        indexfile = local_repo.index_path()
+        tree = local_repo["HEAD"].tree
+        index.build_index_from_tree(local_repo.path, indexfile,
+                                    local_repo.object_store, tree)
+        for f in files:
+            self.assertEqual(os.path.isfile(f), True)
+
+        def determine_wants(*args):
+            return {"refs/heads/master": local_repo.refs["HEAD"]}
+        os.mkdir(os.path.join(self.temp_d, "test"))
+        files = ('testfile11', 'testfile22', 'test/testfile33')
+        i = 0
+        for f in files:
+            file(os.path.join(self.temp_d, f), 'w').write("DATA %s" % i)
+            i += 1
+        local_repo.stage(files)
+        local_repo.do_commit('Test commit', 'fbo@localhost',
+                             ref='refs/heads/master')
+        tcp_client.send_pack("/fakerepo",
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+
+    def test_push_remove_branch(self):
+        def determine_wants(*args):
+            return {"refs/heads/pullr-108": objects.ZERO_SHA,
+                    "refs/heads/master":
+                    local_repo.refs['refs/heads/master'],
+                    "refs/heads/mybranch":
+                    local_repo.refs['refs/heads/mybranch'],
+                    }
+        self.test_push_multiple_branch()
+        local_repo = repo.Repo(self.temp_d)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        tcp_client.send_pack(self.fakerepo,
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+        swift_repo = swift.SwiftRepo("fakerepo", self.conf)
+        self.assertNotIn('refs/heads/pullr-108', swift_repo.refs.allkeys())
+
+    def test_push_annotated_tag(self):
+        def determine_wants(*args):
+            return {"refs/heads/master": local_repo.refs["HEAD"],
+                    "refs/tags/v1.0": local_repo.refs["refs/tags/v1.0"]}
+        local_repo = repo.Repo.init(self.temp_d, mkdir=True)
+        # Nothing in the staging area
+        sha = local_repo.do_commit('Test commit', 'fbo@localhost')
+        otype, data = local_repo.object_store.get_raw(sha)
+        commit = objects.ShaFile.from_raw_string(otype, data)
+        tag = objects.Tag()
+        tag.tagger = "fbo@localhost"
+        tag.message = "Annotated tag"
+        tag.tag_timezone = objects.parse_timezone('-0200')[0]
+        tag.tag_time = commit.author_time
+        tag.object = (objects.Commit, commit.id)
+        tag.name = "v0.1"
+        local_repo.object_store.add_object(tag)
+        local_repo.refs['refs/tags/v1.0'] = tag.id
+        swift.SwiftRepo.init_bare(self.scon, self.conf)
+        tcp_client = client.TCPGitClient(self.server_address,
+                                         port=self.port)
+        tcp_client.send_pack(self.fakerepo,
+                             determine_wants,
+                             local_repo.object_store.generate_pack_contents)
+        swift_repo = swift.SwiftRepo(self.fakerepo, self.conf)
+        tag_sha = swift_repo.refs.read_loose_ref('refs/tags/v1.0')
+        otype, data = swift_repo.object_store.get_raw(tag_sha)
+        rtag = objects.ShaFile.from_raw_string(otype, data)
+        self.assertEqual(rtag.object[1], commit.id)
+        self.assertEqual(rtag.id, tag.id)
+
+
+if __name__ == '__main__':
+    unittest.main()