Merge Dave's fixes for the compatibility tests and web.
authorJelmer Vernooij <jelmer@samba.org>
Sat, 3 Apr 2010 18:26:51 +0000 (20:26 +0200)
committerJelmer Vernooij <jelmer@samba.org>
Sat, 3 Apr 2010 18:26:51 +0000 (20:26 +0200)
Makefile
bin/dul-daemon
bin/dul-web
dulwich/pack.py
dulwich/server.py
dulwich/tests/compat/test_server.py
dulwich/tests/compat/test_web.py
dulwich/tests/test_server.py
dulwich/tests/test_web.py
dulwich/web.py

index 975590c5401287beddc218f37f614dfec9084a3b..c8c7b2941c2820908be44657c2b2731729e9c118 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,7 @@ install::
 
 check:: build
        PYTHONPATH=. $(PYTHON) $(TESTRUNNER) dulwich
+       which git > /dev/null && PYTHONPATH=. $(PYTHON) $(TESTRUNNER) -i compat
 
 check-noextensions:: clean
        PYTHONPATH=. $(PYTHON) $(TESTRUNNER) dulwich
index 2515dc207393e3a2297a8ad745b6cdd02310eb8a..9052e376e61e2e6c3863a5452b6fd7f7ecf9eaca 100755 (executable)
@@ -22,10 +22,11 @@ from dulwich.repo import Repo
 from dulwich.server import DictBackend, GitBackendRepo, TCPGitServer
 
 if __name__ == "__main__":
-    gitdir = None
     if len(sys.argv) > 1:
         gitdir = sys.argv[1]
+    else:
+        gitdir = "."
 
-    backend = DictBackend({"/": GitBackendRepo(Repo(gitdir))})
+    backend = DictBackend({"/": Repo(gitdir)})
     server = TCPGitServer(backend, 'localhost')
     server.serve_forever()
index af728e964d6e1619531970a02d0266b19fe90f0a..2f12648fbd1ad453c060966487e165084ea1bb3a 100644 (file)
@@ -30,7 +30,7 @@ if __name__ == "__main__":
     else:
         gitdir = os.getcwd()
 
-    backend = DictBackend({"/": GitBackendRepo(Repo(gitdir))})
+       backend = DictBackend({"/": Repo(gitdir)})
     app = HTTPGitApplication(backend)
     # TODO: allow serving on other ports via command-line flag
     server = make_server('', 8000, app)
index a5bd501a858b1cb0ca9a1546cd208926c8400526..791d1c434fdf3b1eb6ec9d2239d7978807d83cda 100644 (file)
@@ -36,6 +36,7 @@ except ImportError:
     from misc import defaultdict
 
 import difflib
+import errno
 from itertools import (
     chain,
     imap,
@@ -124,22 +125,41 @@ def load_pack_index(path):
     return load_pack_index_file(path, f)
 
 
+def _load_file_contents(f, size=None):
+    fileno = getattr(f, 'fileno', None)
+    # Attempt to use mmap if possible
+    if fileno is not None:
+        fd = f.fileno()
+        if size is None:
+            size = os.fstat(fd).st_size
+        try:
+            contents = mmap.mmap(fd, size, access=mmap.ACCESS_READ)
+        except mmap.error:
+            # Perhaps a socket?
+            pass
+        else:
+            return contents, size
+    contents = f.read()
+    size = len(contents)
+    return contents, size
+
+
 def load_pack_index_file(path, f):
     """Load an index file from a file-like object.
 
     :param path: Path for the index file
     :param f: File-like object
     """
-    if f.read(4) == '\377tOc':
-        version = struct.unpack(">L", f.read(4))[0]
+    contents, size = _load_file_contents(f)
+    if contents[:4] == '\377tOc':
+        version = struct.unpack(">L", contents[4:8])[0]
         if version == 2:
-            f.seek(0)
-            return PackIndex2(path, file=f)
+            return PackIndex2(path, file=f, contents=contents,
+                size=size)
         else:
             raise KeyError("Unknown pack index format %d" % version)
     else:
-        f.seek(0)
-        return PackIndex1(path, file=f)
+        return PackIndex1(path, file=f, contents=contents, size=size)
 
 
 def bisect_find_sha(start, end, sha, unpack_name):
@@ -179,7 +199,7 @@ class PackIndex(object):
     the start and end offset and then bisect in to find if the value is present.
     """
   
-    def __init__(self, filename, file=None, size=None):
+    def __init__(self, filename, file=None, contents=None, size=None):
         """Create a pack index object.
     
         Provide it with the name of the index file to consider, and it will map
@@ -192,19 +212,10 @@ class PackIndex(object):
             self._file = GitFile(filename, 'rb')
         else:
             self._file = file
-        fileno = getattr(self._file, 'fileno', None)
-        if fileno is not None:
-            fd = self._file.fileno()
-            if size is None:
-                self._size = os.fstat(fd).st_size
-            else:
-                self._size = size
-            self._contents = mmap.mmap(fd, self._size,
-                access=mmap.ACCESS_READ)
+        if contents is None:
+            self._contents, self._size = _load_file_contents(file, size)
         else:
-            self._file.seek(0)
-            self._contents = self._file.read()
-            self._size = len(self._contents)
+            self._contents, self._size = (contents, size)
   
     def __eq__(self, other):
         if not isinstance(other, PackIndex):
@@ -213,7 +224,8 @@ class PackIndex(object):
         if self._fan_out_table != other._fan_out_table:
             return False
     
-        for (name1, _, _), (name2, _, _) in izip(self.iterentries(), other.iterentries()):
+        for (name1, _, _), (name2, _, _) in izip(self.iterentries(),
+                                                 other.iterentries()):
             if name1 != name2:
                 return False
         return True
@@ -265,7 +277,8 @@ class PackIndex(object):
     def iterentries(self):
         """Iterate over the entries in this pack index.
        
-        Will yield tuples with object name, offset in packfile and crc32 checksum.
+        Will yield tuples with object name, offset in packfile and crc32
+        checksum.
         """
         for i in range(len(self)):
             yield self._unpack_entry(i)
@@ -273,7 +286,8 @@ class PackIndex(object):
     def _read_fan_out_table(self, start_offset):
         ret = []
         for i in range(0x100):
-            ret.append(struct.unpack(">L", self._contents[start_offset+i*4:start_offset+(i+1)*4])[0])
+            ret.append(struct.unpack(">L",
+                self._contents[start_offset+i*4:start_offset+(i+1)*4])[0])
         return ret
   
     def check(self):
@@ -305,9 +319,9 @@ class PackIndex(object):
     def object_index(self, sha):
         """Return the index in to the corresponding packfile for the object.
     
-        Given the name of an object it will return the offset that object lives
-        at within the corresponding pack file. If the pack file doesn't have the
-        object then None will be returned.
+        Given the name of an object it will return the offset that object
+        lives at within the corresponding pack file. If the pack file doesn't
+        have the object then None will be returned.
         """
         if len(sha) == 40:
             sha = hex_to_sha(sha)
@@ -335,8 +349,8 @@ class PackIndex(object):
 class PackIndex1(PackIndex):
     """Version 1 Pack Index."""
 
-    def __init__(self, filename, file=None, size=None):
-        PackIndex.__init__(self, filename, file, size)
+    def __init__(self, filename, file=None, contents=None, size=None):
+        PackIndex.__init__(self, filename, file, contents, size)
         self.version = 1
         self._fan_out_table = self._read_fan_out_table(0)
 
@@ -361,8 +375,8 @@ class PackIndex1(PackIndex):
 class PackIndex2(PackIndex):
     """Version 2 Pack Index."""
 
-    def __init__(self, filename, file=None, size=None):
-        PackIndex.__init__(self, filename, file, size)
+    def __init__(self, filename, file=None, contents=None, size=None):
+        PackIndex.__init__(self, filename, file, contents, size)
         assert self._contents[:4] == '\377tOc', "Not a v2 pack index file"
         (self.version, ) = unpack_from(">L", self._contents, 4)
         assert self.version == 2, "Version was %d" % self.version
@@ -472,16 +486,17 @@ class PackData(object):
     buffer from the start of the deflated object on. This is bad, but until I
     get mmap sorted out it will have to do.
   
-    Currently there are no integrity checks done. Also no attempt is made to try
-    and detect the delta case, or a request for an object at the wrong position.
-    It will all just throw a zlib or KeyError.
+    Currently there are no integrity checks done. Also no attempt is made to
+    try and detect the delta case, or a request for an object at the wrong
+    position.  It will all just throw a zlib or KeyError.
     """
   
     def __init__(self, filename, file=None, size=None):
-        """Create a PackData object that represents the pack in the given filename.
+        """Create a PackData object that represents the pack in the given
+        filename.
     
-        The file must exist and stay readable until the object is disposed of. It
-        must also stay the same size. It will be mapped whenever needed.
+        The file must exist and stay readable until the object is disposed of.
+        It must also stay the same size. It will be mapped whenever needed.
     
         Currently there is a restriction on the size of the pack as the python
         mmap implementation is flawed.
@@ -626,7 +641,8 @@ class PackData(object):
             assert isinstance(offset, int)
             assert isinstance(type, int)
             try:
-                type, obj = self.resolve_object(offset, type, obj, get_ref_text)
+                type, obj = self.resolve_object(offset, type, obj,
+                    get_ref_text)
             except Postpone, (sha, ):
                 postponed[sha].append((offset, type, obj))
             else:
@@ -655,8 +671,8 @@ class PackData(object):
         """Create a version 1 file for this data file.
 
         :param filename: Index filename.
-        :param resolve_ext_ref: Function to use for resolving externally referenced
-            SHA1s (for thin packs)
+        :param resolve_ext_ref: Function to use for resolving externally
+            referenced SHA1s (for thin packs)
         :param progress: Progress report function
         """
         entries = self.sorted_entries(resolve_ext_ref, progress=progress)
@@ -666,8 +682,8 @@ class PackData(object):
         """Create a version 2 index file for this data file.
 
         :param filename: Index filename.
-        :param resolve_ext_ref: Function to use for resolving externally referenced
-            SHA1s (for thin packs)
+        :param resolve_ext_ref: Function to use for resolving externally
+            referenced SHA1s (for thin packs)
         :param progress: Progress report function
         """
         entries = self.sorted_entries(resolve_ext_ref, progress=progress)
@@ -678,8 +694,8 @@ class PackData(object):
         """Create an  index file for this data file.
 
         :param filename: Index filename.
-        :param resolve_ext_ref: Function to use for resolving externally referenced
-            SHA1s (for thin packs)
+        :param resolve_ext_ref: Function to use for resolving externally
+            referenced SHA1s (for thin packs)
         :param progress: Progress report function
         """
         if version == 1:
@@ -701,8 +717,8 @@ class PackData(object):
     def get_object_at(self, offset):
         """Given an offset in to the packfile return the object that is there.
     
-        Using the associated index the location of an object can be looked up, and
-        then the packfile can be asked directly for that object using this
+        Using the associated index the location of an object can be looked up,
+        and then the packfile can be asked directly for that object using this
         function.
         """
         if offset in self._offset_cache:
@@ -870,8 +886,8 @@ def write_pack_index_v1(filename, entries, pack_checksum):
     """Write a new pack index file.
 
     :param filename: The filename of the new pack index file.
-    :param entries: List of tuples with object name (sha), offset_in_pack,  and
-            crc32_checksum.
+    :param entries: List of tuples with object name (sha), offset_in_pack,
+        and crc32_checksum.
     :param pack_checksum: Checksum of the pack file.
     """
     f = GitFile(filename, 'wb')
@@ -1019,8 +1035,8 @@ def write_pack_index_v2(filename, entries, pack_checksum):
     """Write a new pack index file.
 
     :param filename: The filename of the new pack index file.
-    :param entries: List of tuples with object name (sha), offset_in_pack,  and
-            crc32_checksum.
+    :param entries: List of tuples with object name (sha), offset_in_pack, and
+        crc32_checksum.
     :param pack_checksum: Checksum of the pack file.
     """
     f = GitFile(filename, 'wb')
index e289458d1957a5bb95c1355e968a2ec4d04a77db..30f0c0873bb1cd80a4a9f7ae1b8beb1827fb0c6a 100644 (file)
@@ -64,9 +64,13 @@ class Backend(object):
 class BackendRepo(object):
     """Repository abstraction used by the Git server.
     
-    Eventually this should become just a subset of Repo.
+    Please note that the methods required here are a 
+    subset of those provided by dulwich.repo.Repo.
     """
 
+    object_store = None
+    refs = None
+
     def get_refs(self):
         """
         Get all the refs in the repository
@@ -86,15 +90,6 @@ class BackendRepo(object):
         """
         return None
 
-    def apply_pack(self, refs, read, delete_refs=True):
-        """ Import a set of changes into a repository and update the refs
-
-        :param refs: list of tuple(name, sha)
-        :param read: callback to read from the incoming pack
-        :param delete_refs: whether to allow deleting refs
-        """
-        raise NotImplementedError
-
     def fetch_objects(self, determine_wants, graph_walker, progress,
                       get_tagged=None):
         """
@@ -107,71 +102,6 @@ class BackendRepo(object):
         raise NotImplementedError
 
 
-class GitBackendRepo(BackendRepo):
-
-    def __init__(self, repo):
-        self.repo = repo
-        self.refs = self.repo.refs
-        self.object_store = self.repo.object_store
-        self.fetch_objects = self.repo.fetch_objects
-        self.get_refs = self.repo.get_refs
-        self.get_peeled = self.repo.get_peeled
-
-    def apply_pack(self, refs, read, delete_refs=True):
-        f, commit = self.repo.object_store.add_thin_pack()
-        all_exceptions = (IOError, OSError, ChecksumMismatch, ApplyDeltaError)
-        status = []
-        unpack_error = None
-        # TODO: more informative error messages than just the exception string
-        try:
-            # TODO: decode the pack as we stream to avoid blocking reads beyond
-            # the end of data (when using HTTP/1.1 chunked encoding)
-            while True:
-                data = read(10240)
-                if not data:
-                    break
-                f.write(data)
-        except all_exceptions, e:
-            unpack_error = str(e).replace('\n', '')
-        try:
-            commit()
-        except all_exceptions, e:
-            if not unpack_error:
-                unpack_error = str(e).replace('\n', '')
-
-        if unpack_error:
-            status.append(('unpack', unpack_error))
-        else:
-            status.append(('unpack', 'ok'))
-
-        for oldsha, sha, ref in refs:
-            ref_error = None
-            try:
-                if sha == ZERO_SHA:
-                    if not delete_refs:
-                        raise GitProtocolError(
-                          'Attempted to delete refs without delete-refs '
-                          'capability.')
-                    try:
-                        del self.repo.refs[ref]
-                    except all_exceptions:
-                        ref_error = 'failed to delete'
-                else:
-                    try:
-                        self.repo.refs[ref] = sha
-                    except all_exceptions:
-                        ref_error = 'failed to write'
-            except KeyError, e:
-                ref_error = 'bad ref'
-            if ref_error:
-                status.append((ref, ref_error))
-            else:
-                status.append((ref, 'ok'))
-
-        print "pack applied"
-        return status
-
-
 class DictBackend(Backend):
     """Trivial backend that looks up Git repositories in a dictionary."""
 
@@ -301,9 +231,9 @@ class UploadPackHandler(Handler):
 class ProtocolGraphWalker(object):
     """A graph walker that knows the git protocol.
 
-    As a graph walker, this class implements ack(), next(), and reset(). It also
-    contains some base methods for interacting with the wire and walking the
-    commit tree.
+    As a graph walker, this class implements ack(), next(), and reset(). It
+    also contains some base methods for interacting with the wire and walking
+    the commit tree.
 
     The work of determining which acks to send is passed on to the
     implementation instance stored in _impl. The reason for this is that we do
@@ -453,10 +383,10 @@ class ProtocolGraphWalker(object):
             commit = pending.popleft()
             if commit.id in haves:
                 return True
-            if not getattr(commit, 'get_parents', None):
+            if commit.type_name != "commit":
                 # non-commit wants are assumed to be satisfied
                 continue
-            for parent in commit.get_parents():
+            for parent in commit.parents:
                 parent_obj = self.store[parent]
                 # TODO: handle parents with later commit times than children
                 if parent_obj.commit_time >= earliest:
@@ -601,6 +531,60 @@ class ReceivePackHandler(Handler):
     def capabilities(self):
         return ("report-status", "delete-refs")
 
+    def _apply_pack(self, refs, read):
+        f, commit = self.repo.object_store.add_thin_pack()
+        all_exceptions = (IOError, OSError, ChecksumMismatch, ApplyDeltaError)
+        status = []
+        unpack_error = None
+        # TODO: more informative error messages than just the exception string
+        try:
+            # TODO: decode the pack as we stream to avoid blocking reads beyond
+            # the end of data (when using HTTP/1.1 chunked encoding)
+            while True:
+                data = read(10240)
+                if not data:
+                    break
+                f.write(data)
+        except all_exceptions, e:
+            unpack_error = str(e).replace('\n', '')
+        try:
+            commit()
+        except all_exceptions, e:
+            if not unpack_error:
+                unpack_error = str(e).replace('\n', '')
+
+        if unpack_error:
+            status.append(('unpack', unpack_error))
+        else:
+            status.append(('unpack', 'ok'))
+
+        for oldsha, sha, ref in refs:
+            ref_error = None
+            try:
+                if sha == ZERO_SHA:
+                    if not self.has_capability('delete-refs'):
+                        raise GitProtocolError(
+                          'Attempted to delete refs without delete-refs '
+                          'capability.')
+                    try:
+                        del self.repo.refs[ref]
+                    except all_exceptions:
+                        ref_error = 'failed to delete'
+                else:
+                    try:
+                        self.repo.refs[ref] = sha
+                    except all_exceptions:
+                        ref_error = 'failed to write'
+            except KeyError, e:
+                ref_error = 'bad ref'
+            if ref_error:
+                status.append((ref, ref_error))
+            else:
+                status.append((ref, 'ok'))
+
+        print "pack applied"
+        return status
+
     def handle(self):
         refs = self.repo.get_refs().items()
 
@@ -636,8 +620,7 @@ class ReceivePackHandler(Handler):
             ref = self.proto.read_pkt_line()
 
         # backend can now deal with this refs and read a pack using self.read
-        status = self.repo.apply_pack(client_refs, self.proto.read,
-            self.has_capability('delete-refs'))
+        status = self.repo._apply_pack(client_refs, self.proto.read)
 
         # when we have read all the pack from the client, send a status report
         # if the client asked for it
index 86a583378e90800fa124e27c7f2e7b5f3048cd0b..1d2847ebf46aaee06614a2591a9f1efeb1b3cba0 100644 (file)
@@ -28,7 +28,6 @@ import threading
 
 from dulwich.server import (
     DictBackend,
-    GitBackendRepo,
     TCPGitServer,
     )
 from server_utils import (
@@ -69,7 +68,7 @@ class GitServerTestCase(ServerTests, CompatTestCase):
         CompatTestCase.tearDown(self)
 
     def _start_server(self, repo):
-        backend = DictBackend({'/': GitBackendRepo(repo)})
+        backend = DictBackend({'/': repo})
         dul_server = TCPGitServer(backend, 'localhost', 0)
         threading.Thread(target=dul_server.serve).start()
         self._server = dul_server
index 36e1522fb8bbd8deeaafced6a74194a6c6026f8c..5a1c038c3a26bb1c8f6bbacdc17fc3853e074c5e 100644 (file)
@@ -29,7 +29,6 @@ from wsgiref import simple_server
 
 from dulwich.server import (
     DictBackend,
-    GitBackendRepo,
     )
 from dulwich.web import (
     HTTPGitApplication,
@@ -69,7 +68,7 @@ class WebTests(ServerTests):
     protocol = 'http'
 
     def _start_server(self, repo):
-        backend = DictBackend({'/': GitBackendRepo(repo)})
+        backend = DictBackend({'/': repo})
         app = self._make_app(backend)
         dul_server = simple_server.make_server('localhost', 0, app,
                                                server_class=WSGIServer)
index bc7d2386dd318c155271880e2fe5415c79c6d010..e13ed2268e56f010b11c39a6ff7c19df3161f85d 100644 (file)
@@ -175,11 +175,9 @@ class TestCommit(object):
 
     def __init__(self, sha, parents, commit_time):
         self.id = sha
-        self._parents = parents
+        self.parents = parents
         self.commit_time = commit_time
-
-    def get_parents(self):
-        return self._parents
+        self.type_name = "commit"
 
     def __repr__(self):
         return '%s(%s)' % (self.__class__.__name__, self._sha)
index aea558a39aa502f48df03bdd237499bd39e6a5f1..17bf499dff32f2ad8a9e33a61cd67927e35b7448 100644 (file)
@@ -23,7 +23,6 @@ import re
 from unittest import TestCase
 
 from dulwich.objects import (
-    Tag,
     Blob,
     )
 from dulwich.web import (
@@ -111,10 +110,10 @@ class DumbHandlersTestCase(WebTestCase):
         tag1 = TestTag('aaa', Blob, '222')
 
         class TestRepo(object):
+
             def __init__(self, objects, peeled):
                 self._objects = dict((o.sha(), o) for o in objects)
                 self._peeled = peeled
-                self.repo = self
 
             def get_peeled(self, sha):
                 return self._peeled[sha]
index 06d19e9a26a745674644087578031aefe02fa372..7b790ba8637c12a987205f8be4c380aa89dd4768 100644 (file)
@@ -59,7 +59,7 @@ def url_prefix(mat):
 
 def get_repo(backend, mat):
     """Get a Repo instance for the given backend and URL regex match."""
-    return backend.open_repository(url_prefix(mat)).repo
+    return backend.open_repository(url_prefix(mat))
 
 
 def send_file(req, f, content_type):