--- /dev/null
+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.
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, "", [])
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"])
"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,
Repo,
)
+try:
+ import gevent
+ import geventhttpclient
+ gevent_support = True
+except ImportError:
+ gevent_support = False
logger = log_utils.getLogger(__name__)
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,
--- /dev/null
+# 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
'server',
'walk',
'web',
+ 'swift',
]
module_names = ['dulwich.tests.test_' + name for name in names]
loader = unittest.TestLoader()
--- /dev/null
+# 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)
--- /dev/null
+# 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()