Forráskód Böngészése

Merge cleanups from Dave.

Jelmer Vernooij 15 éve
szülő
commit
1c98815446

+ 4 - 0
dulwich/_objects.c

@@ -21,6 +21,10 @@
 #include <stdlib.h>
 #include <sys/stat.h>
 
+#if (PY_VERSION_HEX < 0x02050000)
+typedef int Py_ssize_t;
+#endif
+
 #define bytehex(x) (((x)<0xa)?('0'+(x)):('a'-0xa+(x)))
 
 static PyObject *sha_to_pyhex(const unsigned char *sha)

+ 7 - 6
dulwich/file.py

@@ -160,12 +160,13 @@ class _GitFile(object):
             return
         self._file.close()
         try:
-            os.rename(self._lockfilename, self._filename)
-        except OSError, e:
-            # Windows versions prior to Vista don't support atomic renames
-            if e.errno != errno.EEXIST:
-                raise
-            fancy_rename(self._lockfilename, self._filename)
+            try:
+                os.rename(self._lockfilename, self._filename)
+            except OSError, e:
+                # Windows versions prior to Vista don't support atomic renames
+                if e.errno != errno.EEXIST:
+                    raise
+                fancy_rename(self._lockfilename, self._filename)
         finally:
             self.abort()
 

+ 5 - 0
dulwich/misc.py

@@ -30,6 +30,11 @@ try:
 except ImportError:
     from cgi import parse_qs
 
+try:
+    from os import SEEK_END
+except ImportError:
+    SEEK_END = 2
+
 import struct
 
 

+ 7 - 5
dulwich/pack.py

@@ -65,15 +65,16 @@ from dulwich.file import GitFile
 from dulwich.lru_cache import (
     LRUSizeCache,
     )
+from dulwich.misc import (
+    make_sha,
+    SEEK_END,
+    )
 from dulwich.objects import (
     ShaFile,
     hex_to_sha,
     sha_to_hex,
     object_header,
     )
-from dulwich.misc import (
-    make_sha,
-    )
 
 supports_mmap_offset = (sys.version_info[0] >= 3 or
         (sys.version_info[0] == 2 and sys.version_info[1] >= 6))
@@ -129,6 +130,7 @@ def read_zlib_chunks(read_some, dec_size, buffer_size=4096):
     comp_len = fed - len(obj.unused_data)
     return ret, comp_len, obj.unused_data
 
+
 def iter_sha1(iter):
     """Return the hexdigest of the SHA1 over a set of names.
 
@@ -552,7 +554,7 @@ class PackStreamReader(object):
     def _buf_len(self):
         buf = self._rbuf
         start = buf.tell()
-        buf.seek(0, os.SEEK_END)
+        buf.seek(0, SEEK_END)
         end = buf.tell()
         buf.seek(start)
         return end - start
@@ -1383,7 +1385,7 @@ class Pack(object):
         for offset, type, obj, crc32 in self.data.iterobjects():
             assert isinstance(offset, int)
             yield ShaFile.from_raw_chunks(
-                    *self.data.resolve_object(offset, type, obj))
+              *self.data.resolve_object(offset, type, obj))
 
 
 try:

+ 9 - 5
dulwich/protocol.py

@@ -27,6 +27,9 @@ from dulwich.errors import (
     HangupException,
     GitProtocolError,
     )
+from dulwich.misc import (
+    SEEK_END,
+    )
 
 TCP_GIT_PORT = 9418
 
@@ -36,6 +39,7 @@ SINGLE_ACK = 0
 MULTI_ACK = 1
 MULTI_ACK_DETAILED = 2
 
+
 class ProtocolFile(object):
     """
     Some network ops are like file ops. The file ops expect to operate on
@@ -181,7 +185,7 @@ class ReceivableProtocol(Protocol):
 
     def __init__(self, recv, write, report_activity=None, rbufsize=_RBUFSIZE):
         super(ReceivableProtocol, self).__init__(self.read, write,
-                                                report_activity)
+                                                 report_activity)
         self._recv = recv
         self._rbuf = StringIO()
         self._rbufsize = rbufsize
@@ -192,7 +196,7 @@ class ReceivableProtocol(Protocol):
         #  - omit the size <= 0 branch
         #  - seek back to start rather than 0 in case some buffer has been
         #    consumed.
-        #  - use os.SEEK_END instead of the magic number.
+        #  - use SEEK_END instead of the magic number.
         # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
         # Licensed under the Python Software Foundation License.
         # TODO: see if buffer is more efficient than cStringIO.
@@ -203,7 +207,7 @@ class ReceivableProtocol(Protocol):
         # rbufsize is large compared to the typical return value of recv().
         buf = self._rbuf
         start = buf.tell()
-        buf.seek(0, os.SEEK_END)
+        buf.seek(0, SEEK_END)
         # buffer may have been partially consumed by recv()
         buf_len = buf.tell() - start
         if buf_len >= size:
@@ -251,7 +255,7 @@ class ReceivableProtocol(Protocol):
 
         buf = self._rbuf
         start = buf.tell()
-        buf.seek(0, os.SEEK_END)
+        buf.seek(0, SEEK_END)
         buf_len = buf.tell()
         buf.seek(start)
 
@@ -302,7 +306,7 @@ def extract_want_line_capabilities(text):
 def ack_type(capabilities):
     """Extract the ack type from a capabilities list."""
     if 'multi_ack_detailed' in capabilities:
-      return MULTI_ACK_DETAILED
+        return MULTI_ACK_DETAILED
     elif 'multi_ack' in capabilities:
         return MULTI_ACK
     return SINGLE_ACK

+ 14 - 11
dulwich/repo.py

@@ -26,12 +26,12 @@ import errno
 import os
 
 from dulwich.errors import (
-    MissingCommitError, 
+    MissingCommitError,
     NoIndexPresent,
-    NotBlobError, 
-    NotCommitError, 
+    NotBlobError,
+    NotCommitError,
     NotGitRepository,
-    NotTreeError, 
+    NotTreeError,
     NotTagError,
     PackedRefsException,
     )
@@ -270,6 +270,9 @@ class DictRefsContainer(RefsContainer):
     def read_loose_ref(self, name):
         return self._refs[name]
 
+    def __setitem__(self, name, value):
+        self._refs[name] = value
+
 
 class DiskRefsContainer(RefsContainer):
     """Refs container that reads refs from disk."""
@@ -567,7 +570,7 @@ def read_packed_refs(f):
             continue
         if l[0] == "^":
             raise PackedRefsException(
-                "found peeled ref in packed-refs without peeled")
+              "found peeled ref in packed-refs without peeled")
         yield _split_ref_line(l)
 
 
@@ -664,8 +667,8 @@ class BaseRepo(object):
         if determine_wants is None:
             determine_wants = lambda heads: heads.values()
         target.object_store.add_objects(
-            self.fetch_objects(determine_wants, target.get_graph_walker(),
-                progress))
+          self.fetch_objects(determine_wants, target.get_graph_walker(),
+                             progress))
         return self.get_refs()
 
     def fetch_objects(self, determine_wants, graph_walker, progress,
@@ -688,8 +691,8 @@ class BaseRepo(object):
             return []
         haves = self.object_store.find_common_revisions(graph_walker)
         return self.object_store.iter_shas(
-            self.object_store.find_missing_objects(haves, wants, progress,
-                                                   get_tagged))
+          self.object_store.find_missing_objects(haves, wants, progress,
+                                                 get_tagged))
 
     def get_graph_walker(self, heads=None):
         if heads is None:
@@ -924,8 +927,8 @@ class Repo(BaseRepo):
         else:
             raise NotGitRepository(root)
         self.path = root
-        object_store = DiskObjectStore(
-            os.path.join(self.controldir(), OBJECTDIR))
+        object_store = DiskObjectStore(os.path.join(self.controldir(),
+                                                    OBJECTDIR))
         refs = DiskRefsContainer(self.controldir())
         BaseRepo.__init__(self, object_store, refs)
 

+ 28 - 17
dulwich/server.py

@@ -58,6 +58,7 @@ from dulwich.protocol import (
     )
 
 
+
 class Backend(object):
     """A backend for the Git smart server implementation."""
 
@@ -329,10 +330,10 @@ class ProtocolGraphWalker(object):
         while command != None:
             if command != 'want':
                 raise GitProtocolError(
-                    'Protocol got unexpected command %s' % command)
+                  'Protocol got unexpected command %s' % command)
             if sha not in values:
                 raise GitProtocolError(
-                    'Client wants invalid object %s' % sha)
+                  'Client wants invalid object %s' % sha)
             want_revs.append(sha)
             command, sha = self.read_proto_line()
 
@@ -440,10 +441,10 @@ class ProtocolGraphWalker(object):
 
     def set_ack_type(self, ack_type):
         impl_classes = {
-            MULTI_ACK: MultiAckGraphWalkerImpl,
-            MULTI_ACK_DETAILED: MultiAckDetailedGraphWalkerImpl,
-            SINGLE_ACK: SingleAckGraphWalkerImpl,
-            }
+          MULTI_ACK: MultiAckGraphWalkerImpl,
+          MULTI_ACK_DETAILED: MultiAckDetailedGraphWalkerImpl,
+          SINGLE_ACK: SingleAckGraphWalkerImpl,
+          }
         self._impl = impl_classes[ack_type](self)
 
 
@@ -568,7 +569,6 @@ class ReceivePackHandler(Handler):
                           AssertionError, socket.error, zlib.error,
                           ObjectFormatException)
         status = []
-        unpack_error = None
         # TODO: more informative error messages than just the exception string
         try:
             PackStreamCopier(self.proto.read, self.proto.recv, f).verify()
@@ -655,20 +655,26 @@ class ReceivePackHandler(Handler):
             self.proto.write_pkt_line(None)
 
 
+# Default handler classes for git services.
+DEFAULT_HANDLERS = {
+  'git-upload-pack': UploadPackHandler,
+  'git-receive-pack': ReceivePackHandler,
+  }
+
+
 class TCPGitRequestHandler(SocketServer.StreamRequestHandler):
 
+    def __init__(self, handlers, *args, **kwargs):
+        self.handlers = handlers and handlers or DEFAULT_HANDLERS
+        SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs)
+
     def handle(self):
         proto = ReceivableProtocol(self.connection.recv, self.wfile.write)
         command, args = proto.read_cmd()
 
-        # switch case to handle the specific git command
-        if command == 'git-upload-pack':
-            cls = UploadPackHandler
-        elif command == 'git-receive-pack':
-            cls = ReceivePackHandler
-        else:
-            return
-
+        cls = self.handlers.get(command, None)
+        if not callable(cls):
+            raise GitProtocolError('Invalid service %s' % command)
         h = cls(self.server.backend, args, proto)
         h.handle()
 
@@ -678,6 +684,11 @@ class TCPGitServer(SocketServer.TCPServer):
     allow_reuse_address = True
     serve = SocketServer.TCPServer.serve_forever
 
-    def __init__(self, backend, listen_addr, port=TCP_GIT_PORT):
+    def _make_handler(self, *args, **kwargs):
+        return TCPGitRequestHandler(self.handlers, *args, **kwargs)
+
+    def __init__(self, backend, listen_addr, port=TCP_GIT_PORT, handlers=None):
         self.backend = backend
-        SocketServer.TCPServer.__init__(self, (listen_addr, port), TCPGitRequestHandler)
+        self.handlers = handlers
+        SocketServer.TCPServer.__init__(self, (listen_addr, port),
+                                        self._make_handler)

+ 131 - 0
dulwich/tests/compat/test_repository.py

@@ -0,0 +1,131 @@
+# test_repo.py -- Git repo compatibility tests
+# Copyright (C) 2010 Google, Inc.
+#
+# 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.
+
+"""Compatibility tests for dulwich repositories."""
+
+
+from cStringIO import StringIO
+import itertools
+import os
+
+from dulwich.objects import (
+    hex_to_sha,
+    )
+from dulwich.repo import (
+    check_ref_format,
+    )
+from dulwich.tests.utils import (
+    tear_down_repo,
+    )
+
+from utils import (
+    run_git,
+    import_repo,
+    CompatTestCase,
+    )
+
+
+class ObjectStoreTestCase(CompatTestCase):
+    """Tests for git repository compatibility."""
+
+    def setUp(self):
+        CompatTestCase.setUp(self)
+        self._repo = import_repo('server_new.export')
+
+    def tearDown(self):
+        CompatTestCase.tearDown(self)
+        tear_down_repo(self._repo)
+
+    def _run_git(self, args):
+        returncode, output = run_git(args, capture_stdout=True,
+                                     cwd=self._repo.path)
+        self.assertEqual(0, returncode)
+        return output
+
+    def _parse_refs(self, output):
+        refs = {}
+        for line in StringIO(output):
+            fields = line.rstrip('\n').split(' ')
+            self.assertEqual(3, len(fields))
+            refname, type_name, sha = fields
+            check_ref_format(refname[5:])
+            hex_to_sha(sha)
+            refs[refname] = (type_name, sha)
+        return refs
+
+    def _parse_objects(self, output):
+        return set(s.rstrip('\n').split(' ')[0] for s in StringIO(output))
+
+    def test_bare(self):
+        self.assertTrue(self._repo.bare)
+        self.assertFalse(os.path.exists(os.path.join(self._repo.path, '.git')))
+
+    def test_head(self):
+        output = self._run_git(['rev-parse', 'HEAD'])
+        head_sha = output.rstrip('\n')
+        hex_to_sha(head_sha)
+        self.assertEqual(head_sha, self._repo.refs['HEAD'])
+
+    def test_refs(self):
+        output = self._run_git(
+          ['for-each-ref', '--format=%(refname) %(objecttype) %(objectname)'])
+        expected_refs = self._parse_refs(output)
+
+        actual_refs = {}
+        for refname, sha in self._repo.refs.as_dict().iteritems():
+            if refname == 'HEAD':
+                continue  # handled in test_head
+            obj = self._repo[sha]
+            self.assertEqual(sha, obj.id)
+            actual_refs[refname] = (obj.type_name, obj.id)
+        self.assertEqual(expected_refs, actual_refs)
+
+    # TODO(dborowitz): peeled ref tests
+
+    def _get_loose_shas(self):
+        output = self._run_git(['rev-list', '--all', '--objects', '--unpacked'])
+        return self._parse_objects(output)
+
+    def _get_all_shas(self):
+        output = self._run_git(['rev-list', '--all', '--objects'])
+        return self._parse_objects(output)
+
+    def assertShasMatch(self, expected_shas, actual_shas_iter):
+        actual_shas = set()
+        for sha in actual_shas_iter:
+            obj = self._repo[sha]
+            self.assertEqual(sha, obj.id)
+            actual_shas.add(sha)
+        self.assertEqual(expected_shas, actual_shas)
+
+    def test_loose_objects(self):
+        # TODO(dborowitz): This is currently not very useful since fast-imported
+        # repos only contained packed objects.
+        expected_shas = self._get_loose_shas()
+        self.assertShasMatch(expected_shas,
+                             self._repo.object_store._iter_loose_objects())
+
+    def test_packed_objects(self):
+        expected_shas = self._get_all_shas() - self._get_loose_shas()
+        self.assertShasMatch(expected_shas,
+                             itertools.chain(*self._repo.object_store.packs))
+
+    def test_all_objects(self):
+        expected_shas = self._get_all_shas()
+        self.assertShasMatch(expected_shas, iter(self._repo.object_store))

+ 5 - 5
dulwich/tests/test_object_store.py

@@ -20,6 +20,7 @@
 """Tests for the object store interface."""
 
 
+import os
 import shutil
 import tempfile
 from unittest import TestCase
@@ -31,13 +32,12 @@ from dulwich.object_store import (
     DiskObjectStore,
     MemoryObjectStore,
     )
-import os
-import shutil
-import tempfile
+from utils import (
+    make_object,
+    )
 
 
-testobject = Blob()
-testobject.data = "yummy data"
+testobject = make_object(Blob, data="yummy data")
 
 
 class ObjectStoreTests(object):

+ 45 - 39
dulwich/tests/test_objects.py

@@ -50,6 +50,10 @@ from dulwich.objects import (
 from dulwich.tests import (
     TestSkipped,
     )
+from utils import (
+    make_commit,
+    make_object,
+    )
 
 a_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
 b_sha = '2969be3e8ee1c0222396a5611407e4769f14e54b'
@@ -64,12 +68,14 @@ except ImportError:
     # Implementation of permutations from Python 2.6 documentation:
     # http://docs.python.org/2.6/library/itertools.html#itertools.permutations
     # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
+    # Modified syntax slightly to run under Python 2.4.
     def permutations(iterable, r=None):
         # permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC
         # permutations(range(3)) --> 012 021 102 120 201 210
         pool = tuple(iterable)
         n = len(pool)
-        r = n if r is None else r
+        if r is None:
+            r = n
         if r > n:
             return
         indices = range(n)
@@ -245,55 +251,54 @@ class ShaFileCheckTests(unittest.TestCase):
 
 class CommitSerializationTests(unittest.TestCase):
 
-    def make_base(self):
-        c = Commit()
-        c.tree = 'd80c186a03f423a81b39df39dc87fd269736ca86'
-        c.parents = ['ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd', '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6']
-        c.author = 'James Westby <jw+debian@jameswestby.net>'
-        c.committer = 'James Westby <jw+debian@jameswestby.net>'
-        c.commit_time = 1174773719
-        c.author_time = 1174773719
-        c.commit_timezone = 0
-        c.author_timezone = 0
-        c.message =  'Merge ../b\n'
-        return c
+    def make_commit(self, **kwargs):
+        attrs = {'tree': 'd80c186a03f423a81b39df39dc87fd269736ca86',
+                 'parents': ['ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
+                             '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'],
+                 'author': 'James Westby <jw+debian@jameswestby.net>',
+                 'committer': 'James Westby <jw+debian@jameswestby.net>',
+                 'commit_time': 1174773719,
+                 'author_time': 1174773719,
+                 'commit_timezone': 0,
+                 'author_timezone': 0,
+                 'message':  'Merge ../b\n'}
+        attrs.update(kwargs)
+        return make_commit(**attrs)
 
     def test_encoding(self):
-        c = self.make_base()
-        c.encoding = "iso8859-1"
-        self.assertTrue("encoding iso8859-1\n" in c.as_raw_string())        
+        c = self.make_commit(encoding='iso8859-1')
+        self.assertTrue('encoding iso8859-1\n' in c.as_raw_string())
 
     def test_short_timestamp(self):
-        c = self.make_base()
-        c.commit_time = 30
+        c = self.make_commit(commit_time=30)
         c1 = Commit()
         c1.set_raw_string(c.as_raw_string())
         self.assertEquals(30, c1.commit_time)
 
     def test_raw_length(self):
-        c = self.make_base()
+        c = self.make_commit()
         self.assertEquals(len(c.as_raw_string()), c.raw_length())
 
     def test_simple(self):
-        c = self.make_base()
+        c = self.make_commit()
         self.assertEquals(c.id, '5dac377bdded4c9aeb8dff595f0faeebcc8498cc')
         self.assertEquals(
                 'tree d80c186a03f423a81b39df39dc87fd269736ca86\n'
                 'parent ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd\n'
                 'parent 4cffe90e0a41ad3f5190079d7c8f036bde29cbe6\n'
-                'author James Westby <jw+debian@jameswestby.net> 1174773719 +0000\n'
-                'committer James Westby <jw+debian@jameswestby.net> 1174773719 +0000\n'
+                'author James Westby <jw+debian@jameswestby.net> '
+                '1174773719 +0000\n'
+                'committer James Westby <jw+debian@jameswestby.net> '
+                '1174773719 +0000\n'
                 '\n'
                 'Merge ../b\n', c.as_raw_string())
 
     def test_timezone(self):
-        c = self.make_base()
-        c.commit_timezone = 5 * 60
+        c = self.make_commit(commit_timezone=(5 * 60))
         self.assertTrue(" +0005\n" in c.as_raw_string())
 
     def test_neg_timezone(self):
-        c = self.make_base()
-        c.commit_timezone = -1 * 3600
+        c = self.make_commit(commit_timezone=(-1 * 3600))
         self.assertTrue(" -0100\n" in c.as_raw_string())
 
 
@@ -475,19 +480,20 @@ class TreeTests(ShaFileCheckTests):
 class TagSerializeTests(unittest.TestCase):
 
     def test_serialize_simple(self):
-        x = Tag()
-        x.tagger = "Jelmer Vernooij <jelmer@samba.org>"
-        x.name = "0.1"
-        x.message = "Tag 0.1"
-        x.object = (Blob, "d80c186a03f423a81b39df39dc87fd269736ca86")
-        x.tag_time = 423423423
-        x.tag_timezone = 0
-        self.assertEquals("""object d80c186a03f423a81b39df39dc87fd269736ca86
-type blob
-tag 0.1
-tagger Jelmer Vernooij <jelmer@samba.org> 423423423 +0000
-
-Tag 0.1""", x.as_raw_string())
+        x = make_object(Tag,
+                        tagger='Jelmer Vernooij <jelmer@samba.org>',
+                        name='0.1',
+                        message='Tag 0.1',
+                        object=(Blob, 'd80c186a03f423a81b39df39dc87fd269736ca86'),
+                        tag_time=423423423,
+                        tag_timezone=0)
+        self.assertEquals(('object d80c186a03f423a81b39df39dc87fd269736ca86\n'
+                           'type blob\n'
+                           'tag 0.1\n'
+                           'tagger Jelmer Vernooij <jelmer@samba.org> '
+                           '423423423 +0000\n'
+                           '\n'
+                           'Tag 0.1'), x.as_raw_string())
 
 
 default_tagger = ('Linus Torvalds <torvalds@woody.linux-foundation.org> '

+ 2 - 2
dulwich/tests/test_pack.py

@@ -178,8 +178,8 @@ class TestPackData(PackTests):
         blob_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
         tree_data = '100644 a\0%s' % hex_to_sha(blob_sha)
         actual = []
-        for offset, type, chunks, crc32 in p.iterobjects():
-            actual.append((offset, type, ''.join(chunks), crc32))
+        for offset, type_num, chunks, crc32 in p.iterobjects():
+            actual.append((offset, type_num, ''.join(chunks), crc32))
         self.assertEquals([
           (12, 1, commit_data, 3775879613L),
           (138, 2, tree_data, 912998690L),

+ 34 - 34
dulwich/tests/test_repository.py

@@ -343,12 +343,12 @@ class PackedRefsFileTests(unittest.TestCase):
 
     def test_read_with_peeled(self):
         f = StringIO('%s ref/1\n%s ref/2\n^%s\n%s ref/4' % (
-            ONES, TWOS, THREES, FOURS))
+          ONES, TWOS, THREES, FOURS))
         self.assertEqual([
-            (ONES, 'ref/1', None),
-            (TWOS, 'ref/2', THREES),
-            (FOURS, 'ref/4', None),
-            ], list(read_packed_refs_with_peeled(f)))
+          (ONES, 'ref/1', None),
+          (TWOS, 'ref/2', THREES),
+          (FOURS, 'ref/4', None),
+          ], list(read_packed_refs_with_peeled(f)))
 
     def test_read_with_peeled_errors(self):
         f = StringIO('^%s\n%s ref/1' % (TWOS, ONES))
@@ -362,8 +362,8 @@ class PackedRefsFileTests(unittest.TestCase):
         write_packed_refs(f, {'ref/1': ONES, 'ref/2': TWOS},
                           {'ref/1': THREES})
         self.assertEqual(
-            "# pack-refs with: peeled\n%s ref/1\n^%s\n%s ref/2\n" % (
-            ONES, THREES, TWOS), f.getvalue())
+          "# pack-refs with: peeled\n%s ref/1\n^%s\n%s ref/2\n" % (
+          ONES, THREES, TWOS), f.getvalue())
 
     def test_write_without_peeled(self):
         f = StringIO()
@@ -382,9 +382,9 @@ class RefsContainerTests(unittest.TestCase):
 
     def test_get_packed_refs(self):
         self.assertEqual({
-            'refs/heads/packed': '42d06bd4b77fed026b154d16493e5deab78f02ec',
-            'refs/tags/refs-0.1': 'df6800012397fb85c56e7418dd4eb9405dee075c',
-            }, self._refs.get_packed_refs())
+          'refs/heads/packed': '42d06bd4b77fed026b154d16493e5deab78f02ec',
+          'refs/tags/refs-0.1': 'df6800012397fb85c56e7418dd4eb9405dee075c',
+          }, self._refs.get_packed_refs())
 
     def test_get_peeled_not_packed(self):
         # not packed
@@ -402,13 +402,13 @@ class RefsContainerTests(unittest.TestCase):
 
     def test_keys(self):
         self.assertEqual([
-            'HEAD',
-            'refs/heads/loop',
-            'refs/heads/master',
-            'refs/heads/packed',
-            'refs/tags/refs-0.1',
-            'refs/tags/refs-0.2',
-            ], sorted(list(self._refs.keys())))
+          'HEAD',
+          'refs/heads/loop',
+          'refs/heads/master',
+          'refs/heads/packed',
+          'refs/tags/refs-0.1',
+          'refs/tags/refs-0.2',
+          ], sorted(list(self._refs.keys())))
         self.assertEqual(['loop', 'master', 'packed'],
                          sorted(self._refs.keys('refs/heads')))
         self.assertEqual(['refs-0.1', 'refs-0.2'],
@@ -417,12 +417,12 @@ class RefsContainerTests(unittest.TestCase):
     def test_as_dict(self):
         # refs/heads/loop does not show up
         self.assertEqual({
-            'HEAD': '42d06bd4b77fed026b154d16493e5deab78f02ec',
-            'refs/heads/master': '42d06bd4b77fed026b154d16493e5deab78f02ec',
-            'refs/heads/packed': '42d06bd4b77fed026b154d16493e5deab78f02ec',
-            'refs/tags/refs-0.1': 'df6800012397fb85c56e7418dd4eb9405dee075c',
-            'refs/tags/refs-0.2': '3ec9c43c84ff242e3ef4a9fc5bc111fd780a76a8',
-            }, self._refs.as_dict())
+          'HEAD': '42d06bd4b77fed026b154d16493e5deab78f02ec',
+          'refs/heads/master': '42d06bd4b77fed026b154d16493e5deab78f02ec',
+          'refs/heads/packed': '42d06bd4b77fed026b154d16493e5deab78f02ec',
+          'refs/tags/refs-0.1': 'df6800012397fb85c56e7418dd4eb9405dee075c',
+          'refs/tags/refs-0.2': '3ec9c43c84ff242e3ef4a9fc5bc111fd780a76a8',
+          }, self._refs.as_dict())
 
     def test_setitem(self):
         self._refs['refs/some/ref'] = '42d06bd4b77fed026b154d16493e5deab78f02ec'
@@ -455,16 +455,16 @@ class RefsContainerTests(unittest.TestCase):
                          self._refs['HEAD'])
 
         self.assertTrue(self._refs.set_if_equals(
-            'HEAD', '42d06bd4b77fed026b154d16493e5deab78f02ec', nines))
+          'HEAD', '42d06bd4b77fed026b154d16493e5deab78f02ec', nines))
         self.assertEqual(nines, self._refs['HEAD'])
 
         # ensure symref was followed
         self.assertEqual(nines, self._refs['refs/heads/master'])
 
         self.assertFalse(os.path.exists(
-            os.path.join(self._refs.path, 'refs', 'heads', 'master.lock')))
+          os.path.join(self._refs.path, 'refs', 'heads', 'master.lock')))
         self.assertFalse(os.path.exists(
-            os.path.join(self._refs.path, 'HEAD.lock')))
+          os.path.join(self._refs.path, 'HEAD.lock')))
 
     def test_add_if_new(self):
         nines = '9' * 40
@@ -496,11 +496,11 @@ class RefsContainerTests(unittest.TestCase):
 
     def test_follow(self):
         self.assertEquals(
-            ('refs/heads/master', '42d06bd4b77fed026b154d16493e5deab78f02ec'),
-            self._refs._follow('HEAD'))
+          ('refs/heads/master', '42d06bd4b77fed026b154d16493e5deab78f02ec'),
+          self._refs._follow('HEAD'))
         self.assertEquals(
-            ('refs/heads/master', '42d06bd4b77fed026b154d16493e5deab78f02ec'),
-            self._refs._follow('refs/heads/master'))
+          ('refs/heads/master', '42d06bd4b77fed026b154d16493e5deab78f02ec'),
+          self._refs._follow('refs/heads/master'))
         self.assertRaises(KeyError, self._refs._follow, 'notrefs/foo')
         self.assertRaises(KeyError, self._refs._follow, 'refs/heads/loop')
 
@@ -534,9 +534,9 @@ class RefsContainerTests(unittest.TestCase):
 
         # HEAD is a symref, so shouldn't equal its dereferenced value
         self.assertFalse(self._refs.remove_if_equals(
-            'HEAD', '42d06bd4b77fed026b154d16493e5deab78f02ec'))
+          'HEAD', '42d06bd4b77fed026b154d16493e5deab78f02ec'))
         self.assertTrue(self._refs.remove_if_equals(
-            'refs/heads/master', '42d06bd4b77fed026b154d16493e5deab78f02ec'))
+          'refs/heads/master', '42d06bd4b77fed026b154d16493e5deab78f02ec'))
         self.assertRaises(KeyError, lambda: self._refs['refs/heads/master'])
 
         # HEAD is now a broken symref
@@ -553,8 +553,8 @@ class RefsContainerTests(unittest.TestCase):
         self.assertEqual('df6800012397fb85c56e7418dd4eb9405dee075c',
                          self._refs['refs/tags/refs-0.1'])
         self.assertTrue(
-            self._refs.remove_if_equals('refs/tags/refs-0.1',
-            'df6800012397fb85c56e7418dd4eb9405dee075c'))
+          self._refs.remove_if_equals('refs/tags/refs-0.1',
+          'df6800012397fb85c56e7418dd4eb9405dee075c'))
         self.assertRaises(KeyError, lambda: self._refs['refs/tags/refs-0.1'])
 
     def test_read_ref(self):

+ 48 - 46
dulwich/tests/test_server.py

@@ -45,6 +45,7 @@ FOUR = '4' * 40
 FIVE = '5' * 40
 SIX = '6' * 40
 
+
 class TestProto(object):
 
     def __init__(self):
@@ -218,12 +219,12 @@ class ProtocolGraphWalkerTestCase(TestCase):
         #  /
         # 1---2---4
         self._objects = {
-            ONE: TestCommit(ONE, [], 111),
-            TWO: TestCommit(TWO, [ONE], 222),
-            THREE: TestCommit(THREE, [ONE], 333),
-            FOUR: TestCommit(FOUR, [TWO], 444),
-            FIVE: TestCommit(FIVE, [THREE], 555),
-            }
+          ONE: TestCommit(ONE, [], 111),
+          TWO: TestCommit(TWO, [ONE], 222),
+          THREE: TestCommit(THREE, [ONE], 333),
+          FOUR: TestCommit(FOUR, [TWO], 444),
+          FIVE: TestCommit(FIVE, [THREE], 555),
+          }
 
         self._walker = ProtocolGraphWalker(
             TestUploadPackHandler(self._objects, TestProto()),
@@ -256,13 +257,13 @@ class ProtocolGraphWalkerTestCase(TestCase):
 
     def test_read_proto_line(self):
         self._walker.proto.set_output([
-            'want %s' % ONE,
-            'want %s' % TWO,
-            'have %s' % THREE,
-            'foo %s' % FOUR,
-            'bar',
-            'done',
-            ])
+          'want %s' % ONE,
+          'want %s' % TWO,
+          'have %s' % THREE,
+          'foo %s' % FOUR,
+          'bar',
+          'done',
+          ])
         self.assertEquals(('want', ONE), self._walker.read_proto_line())
         self.assertEquals(('want', TWO), self._walker.read_proto_line())
         self.assertEquals(('have', THREE), self._walker.read_proto_line())
@@ -275,9 +276,9 @@ class ProtocolGraphWalkerTestCase(TestCase):
         self.assertRaises(GitProtocolError, self._walker.determine_wants, {})
 
         self._walker.proto.set_output([
-            'want %s multi_ack' % ONE,
-            'want %s' % TWO,
-            ])
+          'want %s multi_ack' % ONE,
+          'want %s' % TWO,
+          ])
         heads = {'ref1': ONE, 'ref2': TWO, 'ref3': THREE}
         self._walker.get_peeled = heads.get
         self.assertEquals([ONE, TWO], self._walker.determine_wants(heads))
@@ -312,11 +313,11 @@ class ProtocolGraphWalkerTestCase(TestCase):
             lines.append(line.rstrip())
 
         self.assertEquals([
-            '%s ref4' % FOUR,
-            '%s ref5' % FIVE,
-            '%s tag6^{}' % FIVE,
-            '%s tag6' % SIX,
-            ], sorted(lines))
+          '%s ref4' % FOUR,
+          '%s ref5' % FIVE,
+          '%s tag6^{}' % FIVE,
+          '%s tag6' % SIX,
+          ], sorted(lines))
 
         # ensure peeled tag was advertised immediately following tag
         for i, line in enumerate(lines):
@@ -359,11 +360,11 @@ class AckGraphWalkerImplTestCase(TestCase):
     def setUp(self):
         self._walker = TestProtocolGraphWalker()
         self._walker.lines = [
-            ('have', TWO),
-            ('have', ONE),
-            ('have', THREE),
-            ('done', None),
-            ]
+          ('have', TWO),
+          ('have', ONE),
+          ('have', THREE),
+          ('done', None),
+          ]
         self._impl = self.impl_cls(self._walker)
 
     def assertNoAck(self):
@@ -451,6 +452,7 @@ class SingleAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
         self.assertNextEquals(None)
         self.assertNak()
 
+
 class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     impl_cls = MultiAckGraphWalkerImpl
@@ -488,17 +490,17 @@ class MultiAckGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
 
     def test_multi_ack_flush(self):
         self._walker.lines = [
-            ('have', TWO),
-            (None, None),
-            ('have', ONE),
-            ('have', THREE),
-            ('done', None),
-            ]
+          ('have', TWO),
+          (None, None),
+          ('have', ONE),
+          ('have', THREE),
+          ('done', None),
+          ]
         self.assertNextEquals(TWO)
         self.assertNoAck()
 
         self.assertNextEquals(ONE)
-        self.assertNak() # nak the flush-pkt
+        self.assertNak()  # nak the flush-pkt
 
         self._walker.done = True
         self._impl.ack(ONE)
@@ -563,17 +565,17 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
     def test_multi_ack_flush(self):
         # same as ack test but contains a flush-pkt in the middle
         self._walker.lines = [
-            ('have', TWO),
-            (None, None),
-            ('have', ONE),
-            ('have', THREE),
-            ('done', None),
-            ]
+          ('have', TWO),
+          (None, None),
+          ('have', ONE),
+          ('have', THREE),
+          ('done', None),
+          ]
         self.assertNextEquals(TWO)
         self.assertNoAck()
 
         self.assertNextEquals(ONE)
-        self.assertNak() # nak the flush-pkt
+        self.assertNak()  # nak the flush-pkt
 
         self._walker.done = True
         self._impl.ack(ONE)
@@ -602,12 +604,12 @@ class MultiAckDetailedGraphWalkerImplTestCase(AckGraphWalkerImplTestCase):
     def test_multi_ack_nak_flush(self):
         # same as nak test but contains a flush-pkt in the middle
         self._walker.lines = [
-            ('have', TWO),
-            (None, None),
-            ('have', ONE),
-            ('have', THREE),
-            ('done', None),
-            ]
+          ('have', TWO),
+          (None, None),
+          ('have', ONE),
+          ('have', THREE),
+          ('done', None),
+          ]
         self.assertNextEquals(TWO)
         self.assertNoAck()
 

+ 37 - 28
dulwich/tests/test_web.py

@@ -40,9 +40,11 @@ from dulwich.web import (
 
 class WebTestCase(TestCase):
     """Base TestCase that sets up some useful instance vars."""
+
     def setUp(self):
         self._environ = {}
-        self._req = HTTPGitRequest(self._environ, self._start_response)
+        self._req = HTTPGitRequest(self._environ, self._start_response,
+                                   handlers=self._handlers())
         self._status = None
         self._headers = []
 
@@ -50,6 +52,9 @@ class WebTestCase(TestCase):
         self._status = status
         self._headers = list(headers)
 
+    def _handlers(self):
+        return None
+
 
 class DumbHandlersTestCase(WebTestCase):
 
@@ -133,16 +138,24 @@ class DumbHandlersTestCase(WebTestCase):
             def __init__(self):
                 objects = [blob1, blob2, blob3, tag1]
                 self.repo = TestRepo(objects, {
-                    'HEAD': '000',
-                    'refs/heads/master': blob1.sha(),
-                    'refs/tags/tag-tag': blob2.sha(),
-                    'refs/tags/blob-tag': blob3.sha(),
-                    })
+                  'HEAD': '000',
+                  'refs/heads/master': blob1.sha(),
+                  'refs/tags/tag-tag': blob2.sha(),
+                  'refs/tags/blob-tag': blob3.sha(),
+                  })
 
             def open_repository(self, path):
                 assert path == '/'
                 return self.repo
 
+            def get_refs(self):
+                return {
+                  'HEAD': '000',
+                  'refs/heads/master': blob1.sha(),
+                  'refs/tags/tag-tag': tag1.sha(),
+                  'refs/tags/blob-tag': blob3.sha(),
+                  }
+
         mat = re.search('.*', '//info/refs')
         self.assertEquals(['111\trefs/heads/master\n',
                            '333\trefs/tags/blob-tag\n',
@@ -164,12 +177,12 @@ class SmartHandlersTestCase(WebTestCase):
         def handle(self):
             self.proto.write('handled input: %s' % self.proto.recv(1024))
 
-    def _MakeHandler(self, *args, **kwargs):
+    def _make_handler(self, *args, **kwargs):
         self._handler = self._TestUploadPackHandler(*args, **kwargs)
         return self._handler
 
-    def services(self):
-        return {'git-upload-pack': self._MakeHandler}
+    def _handlers(self):
+        return {'git-upload-pack': self._make_handler}
 
     def test_handle_service_request_unknown(self):
         mat = re.search('.*', '/git-evil-handler')
@@ -179,8 +192,7 @@ class SmartHandlersTestCase(WebTestCase):
     def test_handle_service_request(self):
         self._environ['wsgi.input'] = StringIO('foo')
         mat = re.search('.*', '/git-upload-pack')
-        output = ''.join(handle_service_request(self._req, 'backend', mat,
-                                                services=self.services()))
+        output = ''.join(handle_service_request(self._req, 'backend', mat))
         self.assertEqual('handled input: foo', output)
         response_type = 'application/x-git-upload-pack-response'
         self.assertTrue(('Content-Type', response_type) in self._headers)
@@ -191,16 +203,14 @@ class SmartHandlersTestCase(WebTestCase):
         self._environ['wsgi.input'] = StringIO('foobar')
         self._environ['CONTENT_LENGTH'] = 3
         mat = re.search('.*', '/git-upload-pack')
-        output = ''.join(handle_service_request(self._req, 'backend', mat,
-                                                services=self.services()))
+        output = ''.join(handle_service_request(self._req, 'backend', mat))
         self.assertEqual('handled input: foo', output)
         response_type = 'application/x-git-upload-pack-response'
         self.assertTrue(('Content-Type', response_type) in self._headers)
 
     def test_get_info_refs_unknown(self):
         self._environ['QUERY_STRING'] = 'service=git-evil-handler'
-        list(get_info_refs(self._req, 'backend', None,
-                           services=self.services()))
+        list(get_info_refs(self._req, 'backend', None))
         self.assertEquals(HTTP_FORBIDDEN, self._status)
 
     def test_get_info_refs(self):
@@ -208,8 +218,7 @@ class SmartHandlersTestCase(WebTestCase):
         self._environ['QUERY_STRING'] = 'service=git-upload-pack'
 
         mat = re.search('.*', '/git-upload-pack')
-        output = ''.join(get_info_refs(self._req, 'backend', mat,
-                                       services=self.services()))
+        output = ''.join(get_info_refs(self._req, 'backend', mat))
         self.assertEquals(('001e# service=git-upload-pack\n'
                            '0000'
                            # input is ignored by the handler
@@ -262,13 +271,13 @@ class HTTPGitRequestTestCase(WebTestCase):
         self._req.respond(status=402, content_type='some/type',
                           headers=[('X-Foo', 'foo'), ('X-Bar', 'bar')])
         self.assertEquals(set([
-            ('X-Foo', 'foo'),
-            ('X-Bar', 'bar'),
-            ('Content-Type', 'some/type'),
-            ('Expires', 'Fri, 01 Jan 1980 00:00:00 GMT'),
-            ('Pragma', 'no-cache'),
-            ('Cache-Control', 'no-cache, max-age=0, must-revalidate'),
-            ]), set(self._headers))
+          ('X-Foo', 'foo'),
+          ('X-Bar', 'bar'),
+          ('Content-Type', 'some/type'),
+          ('Expires', 'Fri, 01 Jan 1980 00:00:00 GMT'),
+          ('Pragma', 'no-cache'),
+          ('Cache-Control', 'no-cache, max-age=0, must-revalidate'),
+          ]), set(self._headers))
         self.assertEquals(402, self._status)
 
 
@@ -285,10 +294,10 @@ class HTTPGitApplicationTestCase(TestCase):
             return 'output'
 
         self._app.services = {
-            ('GET', re.compile('/foo$')): test_handler,
+          ('GET', re.compile('/foo$')): test_handler,
         }
         environ = {
-            'PATH_INFO': '/foo',
-            'REQUEST_METHOD': 'GET',
-            }
+          'PATH_INFO': '/foo',
+          'REQUEST_METHOD': 'GET',
+          }
         self.assertEquals('output', self._app(environ, None))

+ 35 - 0
dulwich/tests/utils.py

@@ -20,10 +20,13 @@
 """Utility functions common to Dulwich tests."""
 
 
+import datetime
 import os
 import shutil
 import tempfile
+import time
 
+from dulwich.objects import Commit
 from dulwich.repo import Repo
 
 
@@ -49,3 +52,35 @@ def tear_down_repo(repo):
     """Tear down a test repository."""
     temp_dir = os.path.dirname(repo.path.rstrip(os.sep))
     shutil.rmtree(temp_dir)
+
+
+def make_object(cls, **attrs):
+    """Make an object for testing and assign some members.
+
+    :param attrs: dict of attributes to set on the new object.
+    :return: A newly initialized object of type cls.
+    """
+    obj = cls()
+    for name, value in attrs.iteritems():
+        setattr(obj, name, value)
+    return obj
+
+
+def make_commit(**attrs):
+    """Make a Commit object with a default set of members.
+
+    :param attrs: dict of attributes to overwrite from the default values.
+    :return: A newly initialized Commit object.
+    """
+    default_time = int(time.mktime(datetime.datetime(2010, 1, 1).timetuple()))
+    all_attrs = {'author': 'Test Author <test@nodomain.com>',
+                 'author_time': default_time,
+                 'author_timezone': 0,
+                 'committer': 'Test Committer <test@nodomain.com>',
+                 'commit_time': default_time,
+                 'commit_timezone': 0,
+                 'message': 'Test message.',
+                 'parents': [],
+                 'tree': '0' * 40}
+    all_attrs.update(attrs)
+    return make_object(Commit, **all_attrs)

+ 45 - 42
dulwich/web.py

@@ -32,8 +32,11 @@ from dulwich.protocol import (
 from dulwich.server import (
     ReceivePackHandler,
     UploadPackHandler,
+    DEFAULT_HANDLERS,
     )
 
+
+# HTTP error strings
 HTTP_OK = '200 OK'
 HTTP_NOT_FOUND = '404 Not Found'
 HTTP_FORBIDDEN = '403 Forbidden'
@@ -80,17 +83,19 @@ def send_file(req, f, content_type):
         yield req.not_found('File not found')
         return
     try:
-        try:
-            req.respond(HTTP_OK, content_type)
-            while True:
-                data = f.read(10240)
-                if not data:
-                    break
-                yield data
-        except IOError:
-            yield req.not_found('Error reading file')
-    finally:
+        req.respond(HTTP_OK, content_type)
+        while True:
+            data = f.read(10240)
+            if not data:
+                break
+            yield data
+        f.close()
+    except IOError:
+        f.close()
+        yield req.not_found('Error reading file')
+    except:
         f.close()
+        raise
 
 
 def get_text_file(req, backend, mat):
@@ -126,15 +131,11 @@ def get_idx_file(req, backend, mat):
                      'application/x-git-packed-objects-toc')
 
 
-default_services = {'git-upload-pack': UploadPackHandler,
-                    'git-receive-pack': ReceivePackHandler}
-def get_info_refs(req, backend, mat, services=None):
-    if services is None:
-        services = default_services
+def get_info_refs(req, backend, mat):
     params = parse_qs(req.environ['QUERY_STRING'])
     service = params.get('service', [None])[0]
     if service and not req.dumb:
-        handler_cls = services.get(service, None)
+        handler_cls = req.handlers.get(service, None)
         if handler_cls is None:
             yield req.forbidden('Unsupported service %s' % service)
             return
@@ -184,6 +185,7 @@ class _LengthLimitedFile(object):
     Content-Length bytes are read. This behavior is required by the WSGI spec
     but not implemented in wsgiref as of 2.5.
     """
+
     def __init__(self, input, max_bytes):
         self._input = input
         self._bytes_avail = max_bytes
@@ -199,11 +201,9 @@ class _LengthLimitedFile(object):
     # TODO: support more methods as necessary
 
 
-def handle_service_request(req, backend, mat, services=None):
-    if services is None:
-        services = default_services
+def handle_service_request(req, backend, mat):
     service = mat.group().lstrip('/')
-    handler_cls = services.get(service, None)
+    handler_cls = req.handlers.get(service, None)
     if handler_cls is None:
         yield req.forbidden('Unsupported service %s' % service)
         return
@@ -230,9 +230,10 @@ class HTTPGitRequest(object):
     :ivar environ: the WSGI environment for the request.
     """
 
-    def __init__(self, environ, start_response, dumb=False):
+    def __init__(self, environ, start_response, dumb=False, handlers=None):
         self.environ = environ
         self.dumb = dumb
+        self.handlers = handlers and handlers or DEFAULT_HANDLERS
         self._start_response = start_response
         self._cache_headers = []
         self._headers = []
@@ -266,19 +267,19 @@ class HTTPGitRequest(object):
     def nocache(self):
         """Set the response to never be cached by the client."""
         self._cache_headers = [
-            ('Expires', 'Fri, 01 Jan 1980 00:00:00 GMT'),
-            ('Pragma', 'no-cache'),
-            ('Cache-Control', 'no-cache, max-age=0, must-revalidate'),
-            ]
+          ('Expires', 'Fri, 01 Jan 1980 00:00:00 GMT'),
+          ('Pragma', 'no-cache'),
+          ('Cache-Control', 'no-cache, max-age=0, must-revalidate'),
+          ]
 
     def cache_forever(self):
         """Set the response to be cached forever by the client."""
         now = time.time()
         self._cache_headers = [
-            ('Date', date_time_string(now)),
-            ('Expires', date_time_string(now + 31536000)),
-            ('Cache-Control', 'public, max-age=31536000'),
-            ]
+          ('Date', date_time_string(now)),
+          ('Expires', date_time_string(now + 31536000)),
+          ('Cache-Control', 'public, max-age=31536000'),
+          ]
 
 
 class HTTPGitApplication(object):
@@ -288,27 +289,29 @@ class HTTPGitApplication(object):
     """
 
     services = {
-        ('GET', re.compile('/HEAD$')): get_text_file,
-        ('GET', re.compile('/info/refs$')): get_info_refs,
-        ('GET', re.compile('/objects/info/alternates$')): get_text_file,
-        ('GET', re.compile('/objects/info/http-alternates$')): get_text_file,
-        ('GET', re.compile('/objects/info/packs$')): get_info_packs,
-        ('GET', re.compile('/objects/([0-9a-f]{2})/([0-9a-f]{38})$')): get_loose_object,
-        ('GET', re.compile('/objects/pack/pack-([0-9a-f]{40})\\.pack$')): get_pack_file,
-        ('GET', re.compile('/objects/pack/pack-([0-9a-f]{40})\\.idx$')): get_idx_file,
-
-        ('POST', re.compile('/git-upload-pack$')): handle_service_request,
-        ('POST', re.compile('/git-receive-pack$')): handle_service_request,
+      ('GET', re.compile('/HEAD$')): get_text_file,
+      ('GET', re.compile('/info/refs$')): get_info_refs,
+      ('GET', re.compile('/objects/info/alternates$')): get_text_file,
+      ('GET', re.compile('/objects/info/http-alternates$')): get_text_file,
+      ('GET', re.compile('/objects/info/packs$')): get_info_packs,
+      ('GET', re.compile('/objects/([0-9a-f]{2})/([0-9a-f]{38})$')): get_loose_object,
+      ('GET', re.compile('/objects/pack/pack-([0-9a-f]{40})\\.pack$')): get_pack_file,
+      ('GET', re.compile('/objects/pack/pack-([0-9a-f]{40})\\.idx$')): get_idx_file,
+
+      ('POST', re.compile('/git-upload-pack$')): handle_service_request,
+      ('POST', re.compile('/git-receive-pack$')): handle_service_request,
     }
 
-    def __init__(self, backend, dumb=False):
+    def __init__(self, backend, dumb=False, handlers=None):
         self.backend = backend
         self.dumb = dumb
+        self.handlers = handlers
 
     def __call__(self, environ, start_response):
         path = environ['PATH_INFO']
         method = environ['REQUEST_METHOD']
-        req = HTTPGitRequest(environ, start_response, self.dumb)
+        req = HTTPGitRequest(environ, start_response, dumb=self.dumb,
+                             handlers=self.handlers)
         # environ['QUERY_STRING'] has qs args
         handler = None
         for smethod, spath in self.services.iterkeys():