← Back to team overview

dulwich-users team mailing list archive

Re: [PATCH] Unified gzip/paster patch

 

On Sun, Apr 24, 2011 at 3:41 PM, Jelmer Vernooij <jelmer@xxxxxxxxx> wrote:
>> Unfortunately, as discussed on IRC, this still fails.
>>
>> It'd be nice if we can land this for 0.7.2. I'm also going to have a
>> look at HTTP smart server support for 0.7.2.
> This patch seems to have stalled; are you still planning to work on it?
> If not, I might have a look to see if I can fix this test.. it'd be a
> shame to have to discard it now.

Sorry for the long delay. Got tied up with work projects. Attached is
a rebased patch to current HEAD. The problem seems to have been that
the egg-info directory wasn't getting created, so the entry points
weren't being found. An updated patch adds a step to the Makefile to
call the egg_info action during the build step.

I also pushed these changes to my github fork.

-- 
Thanks,

David Blewett
diff --git a/Makefile b/Makefile
index 932c8a9..0ab8488 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,7 @@ pydoctor::
 	$(PYDOCTOR) --make-html -c dulwich.cfg
 
 build::
+	$(SETUP) egg_info
 	$(SETUP) build
 	$(SETUP) build_ext -i
 
@@ -34,3 +35,4 @@ check-noextensions:: clean
 clean::
 	$(SETUP) clean --all
 	rm -f dulwich/*.so
+	rm -rf *.egg-info
diff --git a/NEWS b/NEWS
index c5f82c8..517c570 100644
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,24 @@
 
   * Add basic support for alternates. (Jelmer Vernooij, #810429)
 
+  * Added an entry point (dulwich.web.make_paster_app) for Paste.Deploy to be
+    able to determine how to create an instance of HTTPGitApplication.
+    (David Blewett)
+
+  * The WSGI server now transparently handles when a git client submits data
+    using Content-Encoding: gzip.
+    (David Blewett)
+
+  * Added an entry point (dulwich.web.make_paster_app) for Paste.Deploy to be
+    able to determine how to create an instance of HTTPGitApplication.
+    (David Blewett)
+
+  * Added support for Paster to serve instances of HTTPGitApplication.
+    Powered by 3 new entry points: dulwich.contrib.paster.make_app,
+    dulwich.contrib.paster.make_gzip_filter and
+    dulwich.contrib.paster.make_limit_input_filter.
+    (David Blewett)
+
  CHANGES
 
   * unittest2 or python >= 2.7 is now required for the testsuite.
diff --git a/docs/paster.ini b/docs/paster.ini
new file mode 100755
index 0000000..e12ed7f
--- /dev/null
+++ b/docs/paster.ini
@@ -0,0 +1,19 @@
+[app:dulwich]
+use = egg:dulwich
+
+[server:main]
+use = egg:Paste#http
+host = localhost
+port = 8000
+
+[filter:gzip]
+use = egg:dulwich#gzip
+
+[filter:limitinput]
+use = egg:dulwich#limitinput
+
+[pipeline:main]
+pipeline =
+    gzip
+    limitinput
+    dulwich
diff --git a/dulwich/gzip.py b/dulwich/gzip.py
new file mode 100644
index 0000000..48e387d
--- /dev/null
+++ b/dulwich/gzip.py
@@ -0,0 +1,95 @@
+# gzip.py -- Implementation of gzip decoder, using the consumer pattern.
+# GzipConsumer Copyright (C) 1995-2010 by Fredrik Lundh
+#
+# By obtaining, using, and/or copying this software and/or its associated
+# documentation, you agree that you have read, understood, and will comply with
+# the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and its
+# associated documentation for any purpose and without fee is hereby granted,
+# provided that the above copyright notice appears in all copies, and that both
+# that copyright notice and this permission notice appear in supporting
+# documentation, and that the name of Secret Labs AB or the author not be used in
+# advertising or publicity pertaining to distribution of the software without
+# specific, written prior permission.
+#
+# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
+# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
+# NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT
+# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+# DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+# SOFTWARE.
+
+"""Implementation of gzip decoder, using the consumer pattern."""
+
+from cStringIO import StringIO
+
+class StringConsumer(object):
+
+    def __init__(self):
+        self._data = StringIO()
+
+    def feed(self, data):
+        self._data.write(data)
+
+    def close(self):
+        # We don't want to close the underlying StringIO instance
+        return self._data
+
+# The below courtesy of Fredrik Lundh
+# http://effbot.org/zone/consumer-gzip.htm
+class GzipConsumer(object):
+    """Consumer class to provide gzip decoding on the fly.
+    The consumer acts like a filter, passing decoded data on to another
+    consumer object.
+    """
+    def __init__(self, consumer=None):
+        if consumer is None:
+            consumer = StringConsumer()
+        self._consumer = consumer
+        self._decoder = None
+        self._data = ''
+
+    def feed(self, data):
+        if self._decoder is None:
+            # check if we have a full gzip header
+            data = self._data + data
+            try:
+                i = 10
+                flag = ord(data[3])
+                if flag & 4: # extra
+                    x = ord(data[i]) + 256*ord(data[i+1])
+                    i = i + 2 + x
+                if flag & 8: # filename
+                    while ord(data[i]):
+                        i = i + 1
+                    i = i + 1
+                if flag & 16: # comment
+                    while ord(data[i]):
+                        i = i + 1
+                    i = i + 1
+                if flag & 2: # crc
+                    i = i + 2
+                if len(data) < i:
+                    raise IndexError('not enough data')
+                if data[:3] != '\x1f\x8b\x08':
+                    raise IOError('invalid gzip data')
+                data = data[i:]
+            except IndexError:
+                self.__data = data
+                return # need more data
+            import zlib
+            self._data = ''
+            self._decoder = zlib.decompressobj(-zlib.MAX_WBITS)
+        data = self._decoder.decompress(data)
+        if data:
+            self._consumer.feed(data)
+
+    def close(self):
+        if self._decoder:
+            data = self._decoder.flush()
+            if data:
+                self._consumer.feed(data)
+        return self._consumer.close()
+
diff --git a/dulwich/tests/compat/test_web.py b/dulwich/tests/compat/test_web.py
index cebe768..bbe66b2 100644
--- a/dulwich/tests/compat/test_web.py
+++ b/dulwich/tests/compat/test_web.py
@@ -36,6 +36,7 @@ from dulwich.tests import (
 from dulwich.web import (
     HTTPGitApplication,
     HTTPGitRequestHandler,
+    make_wsgi_chain,
     )
 
 from dulwich.tests.compat.server_utils import (
@@ -101,8 +102,12 @@ class SmartWebTestCase(WebTests, CompatTestCase):
         self.assertFalse('side-band-64k' in caps)
 
     def _make_app(self, backend):
-        app = HTTPGitApplication(backend, handlers=self._handlers())
-        self._check_app(app)
+        app = make_wsgi_chain(backend, handlers=self._handlers())
+        to_check = app
+        # peel back layers until we're at the base application
+        while not issubclass(to_check.__class__, HTTPGitApplication):
+            to_check = to_check.app
+        self._check_app(to_check)
         return app
 
 
@@ -125,7 +130,7 @@ class DumbWebTestCase(WebTests, CompatTestCase):
     """Test cases for dumb HTTP server."""
 
     def _make_app(self, backend):
-        return HTTPGitApplication(backend, dumb=True)
+        return make_wsgi_chain(backend, dumb=True)
 
     def test_push_to_dulwich(self):
         # Note: remove this if dumb pushing is supported
diff --git a/dulwich/tests/test_web.py b/dulwich/tests/test_web.py
index 7d0ed3d..7197d92 100644
--- a/dulwich/tests/test_web.py
+++ b/dulwich/tests/test_web.py
@@ -19,8 +19,14 @@
 """Tests for the Git HTTP server."""
 
 from cStringIO import StringIO
+import gzip
+import os
 import re
+import shutil
 
+from dulwich.tests.compat.utils import (
+    import_repo_to_dir,
+    )
 from dulwich.object_store import (
     MemoryObjectStore,
     )
@@ -37,6 +43,7 @@ from dulwich.server import (
     )
 from dulwich.tests import (
     TestCase,
+    SkipTest,
     )
 from dulwich.web import (
     HTTP_OK,
@@ -52,14 +59,23 @@ from dulwich.web import (
     get_info_packs,
     handle_service_request,
     _LengthLimitedFile,
+    GunzipFilter,
+    LimitedInputFilter,
     HTTPGitRequest,
     HTTPGitApplication,
     )
+from dulwich.web.paster import (
+    make_app,
+    make_gzip_filter,
+    make_limit_input_filter,
+)
 
 from dulwich.tests.utils import (
     make_object,
     )
 
+_BASE_PKG_DIR = os.path.abspath(os.path.join(
+    os.path.dirname(__file__), os.pardir, os.pardir))
 
 class TestHTTPGitRequest(HTTPGitRequest):
     """HTTPGitRequest with overridden methods to help test caching."""
@@ -417,20 +433,143 @@ class HTTPGitApplicationTestCase(TestCase):
     def setUp(self):
         super(HTTPGitApplicationTestCase, self).setUp()
         self._app = HTTPGitApplication('backend')
+        self._environ = {
+            'PATH_INFO': '/foo',
+            'REQUEST_METHOD': 'GET',
+        }
+
+    def _test_handler(self, req, backend, mat):
+        # tests interface used by all handlers
+        self.assertEquals(self._environ, req.environ)
+        self.assertEquals('backend', backend)
+        self.assertEquals('/foo', mat.group(0))
+        return 'output'
+
+    def _add_handler(self, app):
+        req = self._environ['REQUEST_METHOD']
+        app.services = {
+          (req, re.compile('/foo$')): self._test_handler,
+        }
+
+    def test_call(self):
+        self._add_handler(self._app)
+        self.assertEquals('output', self._app(self._environ, None))
+
+class GunzipTestCase(HTTPGitApplicationTestCase):
+    """TestCase for testing the GunzipFilter, ensuring the wsgi.input
+    is correctly decompressed and headers are corrected.
+    """
+
+    def setUp(self):
+        super(GunzipTestCase, self).setUp()
+        self._app = GunzipFilter(self._app)
+        self._environ['HTTP_CONTENT_ENCODING'] = 'gzip'
+        self._environ['REQUEST_METHOD'] = 'POST'
+
+    def _get_zstream(self, text):
+        zstream = StringIO()
+        zfile = gzip.GzipFile(fileobj=zstream, mode='w')
+        zfile.write(text)
+        zfile.close()
+        return zstream
 
     def test_call(self):
-        def test_handler(req, backend, mat):
-            # tests interface used by all handlers
-            self.assertEquals(environ, req.environ)
-            self.assertEquals('backend', backend)
-            self.assertEquals('/foo', mat.group(0))
-            return 'output'
-
-        self._app.services = {
-          ('GET', re.compile('/foo$')): test_handler,
+        self._add_handler(self._app.app)
+        orig = self.__class__.__doc__
+        zstream = self._get_zstream(orig)
+        zlength = zstream.tell()
+        zstream.seek(0)
+        self.assertLess(zlength, len(orig))
+        self.assertEquals(self._environ['HTTP_CONTENT_ENCODING'],
+                          'gzip')
+        self._environ['CONTENT_LENGTH'] = zlength
+        self._environ['wsgi.input'] = zstream
+        app_output = self._app(self._environ, None)
+        buf = self._environ['wsgi.input']
+        self.assertIsNot(buf, zstream)
+        buf.seek(0)
+        self.assertEquals(orig, buf.read())
+        self.assertLess(zlength, int(self._environ['CONTENT_LENGTH']))
+        self.assertNotIn('HTTP_CONTENT_ENCODING', self._environ)
+
+class PasterFactoryTests(TestCase):
+    """Tests for the Paster factory and filter functions."""
+
+    def setUp(self):
+        super(PasterFactoryTests, self).setUp()
+        self.global_config = {'__file__': '/path/to/paster.ini'}
+        self.repo_dirs = []
+        self.repo_names = ('server_new.export', 'server_old.export')
+        self.entry_points = {
+            'main': make_app,
+            'gzip': make_gzip_filter,
+            'limitinput': make_limit_input_filter,
         }
-        environ = {
-          'PATH_INFO': '/foo',
-          'REQUEST_METHOD': 'GET',
-          }
-        self.assertEquals('output', self._app(environ, None))
+        for rname in self.repo_names:
+            self.repo_dirs.append(import_repo_to_dir(rname))
+        # Test import to see if paste.deploy is available
+        try:
+            from paste.deploy.converters import asbool
+            from pkg_resources import WorkingSet
+            self.working_set = WorkingSet()
+            self.working_set.add_entry(_BASE_PKG_DIR)
+        except ImportError:
+            raise SkipTest('paste.deploy not available')
+
+    def tearDown(self):
+        super(PasterFactoryTests, self).setUp()
+        for rdir in self.repo_dirs:
+            shutil.rmtree(rdir)
+
+    def test_cwd(self):
+        cwd = os.getcwd()
+        os.chdir(self.repo_dirs[0])
+        app = make_app(self.global_config)
+        os.chdir(cwd)
+        self.assertIn('/', app.backend.repos)
+
+    def test_badrepo(self):
+        self.assertRaises(IndexError, make_app, self.global_config, foo='/')
+
+    def test_repo(self):
+        rname = self.repo_names[0]
+        local_config = {rname: self.repo_dirs[0]}
+        app = make_app(self.global_config, **local_config)
+        self.assertIn('/%s' % rname, app.backend.repos)
+
+    def _get_repo_parents(self):
+        repo_parents = []
+        for rdir in self.repo_dirs:
+            repo_parents.append(os.path.split(rdir)[0])
+        return repo_parents
+
+    def test_append_git(self):
+        app = make_app(self.global_config, append_git=True,
+                       serve_dirs=self._get_repo_parents())
+        for rname in self.repo_names:
+            self.assertIn('/%s.git' % rname, app.backend.repos)
+
+    def test_serve_dirs(self):
+        app = make_app(self.global_config, serve_dirs=self._get_repo_parents())
+        for rname in self.repo_names:
+            self.assertIn('/%s' % rname, app.backend.repos)
+
+    def _test_wrap(self, factory, wrapper):
+        app = make_app(self.global_config, serve_dirs=self._get_repo_parents())
+        wrapped_app = factory(self.global_config)(app)
+        self.assertTrue(isinstance(wrapped_app, wrapper))
+
+    def test_make_gzip_filter(self):
+        self._test_wrap(make_gzip_filter, GunzipFilter)
+
+    def test_make_limit_input_filter(self):
+        self._test_wrap(make_limit_input_filter, LimitedInputFilter)
+
+    def test_entry_points(self):
+        test_points = {}
+        for group in ('paste.app_factory', 'paste.filter_factory'):
+            for ep in self.working_set.iter_entry_points(group):
+                test_points[ep.name] = ep.load()
+
+        for ep_name, ep in self.entry_points.items():
+            self.assertTrue(test_points[ep_name] is ep)
diff --git a/dulwich/web.py b/dulwich/web.py
deleted file mode 100644
index 19e6e85..0000000
--- a/dulwich/web.py
+++ /dev/null
@@ -1,415 +0,0 @@
-# web.py -- WSGI smart-http server
-# 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
-# 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.
-
-"""HTTP server for dulwich that implements the git smart HTTP protocol."""
-
-from cStringIO import StringIO
-import os
-import re
-import sys
-import time
-
-try:
-    from urlparse import parse_qs
-except ImportError:
-    from dulwich._compat import parse_qs
-from dulwich import log_utils
-from dulwich.protocol import (
-    ReceivableProtocol,
-    )
-from dulwich.repo import (
-    Repo,
-    )
-from dulwich.server import (
-    DictBackend,
-    DEFAULT_HANDLERS,
-    )
-
-
-logger = log_utils.getLogger(__name__)
-
-
-# HTTP error strings
-HTTP_OK = '200 OK'
-HTTP_NOT_FOUND = '404 Not Found'
-HTTP_FORBIDDEN = '403 Forbidden'
-HTTP_ERROR = '500 Internal Server Error'
-
-
-def date_time_string(timestamp=None):
-    # From BaseHTTPRequestHandler.date_time_string in BaseHTTPServer.py in the
-    # Python 2.6.5 standard library, following modifications:
-    #  - Made a global rather than an instance method.
-    #  - weekdayname and monthname are renamed and locals rather than class
-    #    variables.
-    # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
-    weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-    months = [None,
-              'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
-              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
-    if timestamp is None:
-        timestamp = time.time()
-    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
-    return '%s, %02d %3s %4d %02d:%02d:%02d GMD' % (
-            weekdays[wd], day, months[month], year, hh, mm, ss)
-
-
-def url_prefix(mat):
-    """Extract the URL prefix from a regex match.
-
-    :param mat: A regex match object.
-    :returns: The URL prefix, defined as the text before the match in the
-        original string. Normalized to start with one leading slash and end with
-        zero.
-    """
-    return '/' + mat.string[:mat.start()].strip('/')
-
-
-def get_repo(backend, mat):
-    """Get a Repo instance for the given backend and URL regex match."""
-    return backend.open_repository(url_prefix(mat))
-
-
-def send_file(req, f, content_type):
-    """Send a file-like object to the request output.
-
-    :param req: The HTTPGitRequest object to send output to.
-    :param f: An open file-like object to send; will be closed.
-    :param content_type: The MIME type for the file.
-    :return: Iterator over the contents of the file, as chunks.
-    """
-    if f is None:
-        yield req.not_found('File not found')
-        return
-    try:
-        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.error('Error reading file')
-    except:
-        f.close()
-        raise
-
-
-def _url_to_path(url):
-    return url.replace('/', os.path.sep)
-
-
-def get_text_file(req, backend, mat):
-    req.nocache()
-    path = _url_to_path(mat.group())
-    logger.info('Sending plain text file %s', path)
-    return send_file(req, get_repo(backend, mat).get_named_file(path),
-                     'text/plain')
-
-
-def get_loose_object(req, backend, mat):
-    sha = mat.group(1) + mat.group(2)
-    logger.info('Sending loose object %s', sha)
-    object_store = get_repo(backend, mat).object_store
-    if not object_store.contains_loose(sha):
-        yield req.not_found('Object not found')
-        return
-    try:
-        data = object_store[sha].as_legacy_object()
-    except IOError:
-        yield req.error('Error reading object')
-        return
-    req.cache_forever()
-    req.respond(HTTP_OK, 'application/x-git-loose-object')
-    yield data
-
-
-def get_pack_file(req, backend, mat):
-    req.cache_forever()
-    path = _url_to_path(mat.group())
-    logger.info('Sending pack file %s', path)
-    return send_file(req, get_repo(backend, mat).get_named_file(path),
-                     'application/x-git-packed-objects')
-
-
-def get_idx_file(req, backend, mat):
-    req.cache_forever()
-    path = _url_to_path(mat.group())
-    logger.info('Sending pack file %s', path)
-    return send_file(req, get_repo(backend, mat).get_named_file(path),
-                     'application/x-git-packed-objects-toc')
-
-
-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 = req.handlers.get(service, None)
-        if handler_cls is None:
-            yield req.forbidden('Unsupported service %s' % service)
-            return
-        req.nocache()
-        write = req.respond(HTTP_OK, 'application/x-%s-advertisement' % service)
-        proto = ReceivableProtocol(StringIO().read, write)
-        handler = handler_cls(backend, [url_prefix(mat)], proto,
-                              http_req=req, advertise_refs=True)
-        handler.proto.write_pkt_line('# service=%s\n' % service)
-        handler.proto.write_pkt_line(None)
-        handler.handle()
-    else:
-        # non-smart fallback
-        # TODO: select_getanyfile() (see http-backend.c)
-        req.nocache()
-        req.respond(HTTP_OK, 'text/plain')
-        logger.info('Emulating dumb info/refs')
-        repo = get_repo(backend, mat)
-        refs = repo.get_refs()
-        for name in sorted(refs.iterkeys()):
-            # get_refs() includes HEAD as a special case, but we don't want to
-            # advertise it
-            if name == 'HEAD':
-                continue
-            sha = refs[name]
-            o = repo[sha]
-            if not o:
-                continue
-            yield '%s\t%s\n' % (sha, name)
-            peeled_sha = repo.get_peeled(name)
-            if peeled_sha != sha:
-                yield '%s\t%s^{}\n' % (peeled_sha, name)
-
-
-def get_info_packs(req, backend, mat):
-    req.nocache()
-    req.respond(HTTP_OK, 'text/plain')
-    logger.info('Emulating dumb info/packs')
-    for pack in get_repo(backend, mat).object_store.packs:
-        yield 'P pack-%s.pack\n' % pack.name()
-
-
-class _LengthLimitedFile(object):
-    """Wrapper class to limit the length of reads from a file-like object.
-
-    This is used to ensure EOF is read from the wsgi.input object once
-    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
-
-    def read(self, size=-1):
-        if self._bytes_avail <= 0:
-            return ''
-        if size == -1 or size > self._bytes_avail:
-            size = self._bytes_avail
-        self._bytes_avail -= size
-        return self._input.read(size)
-
-    # TODO: support more methods as necessary
-
-
-def handle_service_request(req, backend, mat):
-    service = mat.group().lstrip('/')
-    logger.info('Handling service request for %s', service)
-    handler_cls = req.handlers.get(service, None)
-    if handler_cls is None:
-        yield req.forbidden('Unsupported service %s' % service)
-        return
-    req.nocache()
-    write = req.respond(HTTP_OK, 'application/x-%s-result' % service)
-
-    input = req.environ['wsgi.input']
-    # This is not necessary if this app is run from a conforming WSGI server.
-    # Unfortunately, there's no way to tell that at this point.
-    # TODO: git may used HTTP/1.1 chunked encoding instead of specifying
-    # content-length
-    content_length = req.environ.get('CONTENT_LENGTH', '')
-    if content_length:
-        input = _LengthLimitedFile(input, int(content_length))
-    proto = ReceivableProtocol(input.read, write)
-    handler = handler_cls(backend, [url_prefix(mat)], proto, http_req=req)
-    handler.handle()
-
-
-class HTTPGitRequest(object):
-    """Class encapsulating the state of a single git HTTP request.
-
-    :ivar environ: the WSGI environment for the request.
-    """
-
-    def __init__(self, environ, start_response, dumb=False, handlers=None):
-        self.environ = environ
-        self.dumb = dumb
-        self.handlers = handlers
-        self._start_response = start_response
-        self._cache_headers = []
-        self._headers = []
-
-    def add_header(self, name, value):
-        """Add a header to the response."""
-        self._headers.append((name, value))
-
-    def respond(self, status=HTTP_OK, content_type=None, headers=None):
-        """Begin a response with the given status and other headers."""
-        if headers:
-            self._headers.extend(headers)
-        if content_type:
-            self._headers.append(('Content-Type', content_type))
-        self._headers.extend(self._cache_headers)
-
-        return self._start_response(status, self._headers)
-
-    def not_found(self, message):
-        """Begin a HTTP 404 response and return the text of a message."""
-        self._cache_headers = []
-        logger.info('Not found: %s', message)
-        self.respond(HTTP_NOT_FOUND, 'text/plain')
-        return message
-
-    def forbidden(self, message):
-        """Begin a HTTP 403 response and return the text of a message."""
-        self._cache_headers = []
-        logger.info('Forbidden: %s', message)
-        self.respond(HTTP_FORBIDDEN, 'text/plain')
-        return message
-
-    def error(self, message):
-        """Begin a HTTP 500 response and return the text of a message."""
-        self._cache_headers = []
-        logger.error('Error: %s', message)
-        self.respond(HTTP_ERROR, 'text/plain')
-        return message
-
-    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'),
-          ]
-
-    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'),
-          ]
-
-
-class HTTPGitApplication(object):
-    """Class encapsulating the state of a git WSGI application.
-
-    :ivar backend: the Backend object backing this application
-    """
-
-    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,
-    }
-
-    def __init__(self, backend, dumb=False, handlers=None):
-        self.backend = backend
-        self.dumb = dumb
-        self.handlers = dict(DEFAULT_HANDLERS)
-        if handlers is not None:
-            self.handlers.update(handlers)
-
-    def __call__(self, environ, start_response):
-        path = environ['PATH_INFO']
-        method = environ['REQUEST_METHOD']
-        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():
-            if smethod != method:
-                continue
-            mat = spath.search(path)
-            if mat:
-                handler = self.services[smethod, spath]
-                break
-        if handler is None:
-            return req.not_found('Sorry, that method is not supported')
-        return handler(req, self.backend, mat)
-
-
-# The reference server implementation is based on wsgiref, which is not
-# distributed with python 2.4. If wsgiref is not present, users will not be able
-# to use the HTTP server without a little extra work.
-try:
-    from wsgiref.simple_server import (
-        WSGIRequestHandler,
-        make_server,
-        )
-
-    class HTTPGitRequestHandler(WSGIRequestHandler):
-        """Handler that uses dulwich's logger for logging exceptions."""
-
-        def log_exception(self, exc_info):
-            logger.exception('Exception happened during processing of request',
-                             exc_info=exc_info)
-
-        def log_message(self, format, *args):
-            logger.info(format, *args)
-
-        def log_error(self, *args):
-            logger.error(*args)
-
-
-    def main(argv=sys.argv):
-        """Entry point for starting an HTTP git server."""
-        if len(argv) > 1:
-            gitdir = argv[1]
-        else:
-            gitdir = os.getcwd()
-
-        # TODO: allow serving on other addresses/ports via command-line flag
-        listen_addr=''
-        port = 8000
-
-        log_utils.default_logging_config()
-        backend = DictBackend({'/': Repo(gitdir)})
-        app = HTTPGitApplication(backend)
-        server = make_server(listen_addr, port, app,
-                             handler_class=HTTPGitRequestHandler)
-        logger.info('Listening for HTTP connections on %s:%d', listen_addr,
-                    port)
-        server.serve_forever()
-
-except ImportError:
-    # No wsgiref found; don't provide the reference functionality, but leave the
-    # rest of the WSGI-based implementation.
-    def main(argv=sys.argv):
-        """Stub entry point for failing to start a server without wsgiref."""
-        sys.stderr.write('Sorry, the wsgiref module is required for dul-web.\n')
-        sys.exit(1)
diff --git a/dulwich/web/__init__.py b/dulwich/web/__init__.py
new file mode 100644
index 0000000..860e9bd
--- /dev/null
+++ b/dulwich/web/__init__.py
@@ -0,0 +1,465 @@
+# web.py -- WSGI smart-http server
+# 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
+# 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.
+
+"""HTTP server for dulwich that implements the git smart HTTP protocol."""
+
+from cStringIO import StringIO
+import os
+import re
+import sys
+import time
+
+try:
+    from urlparse import parse_qs
+except ImportError:
+    from dulwich._compat import parse_qs
+from dulwich import log_utils
+from dulwich.gzip import GzipConsumer
+from dulwich.protocol import (
+    ReceivableProtocol,
+    )
+from dulwich.repo import (
+    Repo,
+    )
+from dulwich.server import (
+    DictBackend,
+    DEFAULT_HANDLERS,
+    )
+
+
+logger = log_utils.getLogger(__name__)
+
+
+# HTTP error strings
+HTTP_OK = '200 OK'
+HTTP_NOT_FOUND = '404 Not Found'
+HTTP_FORBIDDEN = '403 Forbidden'
+HTTP_ERROR = '500 Internal Server Error'
+
+
+def date_time_string(timestamp=None):
+    # From BaseHTTPRequestHandler.date_time_string in BaseHTTPServer.py in the
+    # Python 2.6.5 standard library, following modifications:
+    #  - Made a global rather than an instance method.
+    #  - weekdayname and monthname are renamed and locals rather than class
+    #    variables.
+    # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
+    weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+    months = [None,
+              'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+    if timestamp is None:
+        timestamp = time.time()
+    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
+    return '%s, %02d %3s %4d %02d:%02d:%02d GMD' % (
+            weekdays[wd], day, months[month], year, hh, mm, ss)
+
+
+def url_prefix(mat):
+    """Extract the URL prefix from a regex match.
+
+    :param mat: A regex match object.
+    :returns: The URL prefix, defined as the text before the match in the
+        original string. Normalized to start with one leading slash and end with
+        zero.
+    """
+    return '/' + mat.string[:mat.start()].strip('/')
+
+
+def get_repo(backend, mat):
+    """Get a Repo instance for the given backend and URL regex match."""
+    return backend.open_repository(url_prefix(mat))
+
+
+def send_file(req, f, content_type):
+    """Send a file-like object to the request output.
+
+    :param req: The HTTPGitRequest object to send output to.
+    :param f: An open file-like object to send; will be closed.
+    :param content_type: The MIME type for the file.
+    :return: Iterator over the contents of the file, as chunks.
+    """
+    if f is None:
+        yield req.not_found('File not found')
+        return
+    try:
+        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.error('Error reading file')
+    except:
+        f.close()
+        raise
+
+
+def _url_to_path(url):
+    return url.replace('/', os.path.sep)
+
+
+def get_text_file(req, backend, mat):
+    req.nocache()
+    path = _url_to_path(mat.group())
+    logger.info('Sending plain text file %s', path)
+    return send_file(req, get_repo(backend, mat).get_named_file(path),
+                     'text/plain')
+
+
+def get_loose_object(req, backend, mat):
+    sha = mat.group(1) + mat.group(2)
+    logger.info('Sending loose object %s', sha)
+    object_store = get_repo(backend, mat).object_store
+    if not object_store.contains_loose(sha):
+        yield req.not_found('Object not found')
+        return
+    try:
+        data = object_store[sha].as_legacy_object()
+    except IOError:
+        yield req.error('Error reading object')
+        return
+    req.cache_forever()
+    req.respond(HTTP_OK, 'application/x-git-loose-object')
+    yield data
+
+
+def get_pack_file(req, backend, mat):
+    req.cache_forever()
+    path = _url_to_path(mat.group())
+    logger.info('Sending pack file %s', path)
+    return send_file(req, get_repo(backend, mat).get_named_file(path),
+                     'application/x-git-packed-objects')
+
+
+def get_idx_file(req, backend, mat):
+    req.cache_forever()
+    path = _url_to_path(mat.group())
+    logger.info('Sending pack file %s', path)
+    return send_file(req, get_repo(backend, mat).get_named_file(path),
+                     'application/x-git-packed-objects-toc')
+
+
+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 = req.handlers.get(service, None)
+        if handler_cls is None:
+            yield req.forbidden('Unsupported service %s' % service)
+            return
+        req.nocache()
+        write = req.respond(HTTP_OK, 'application/x-%s-advertisement' % service)
+        proto = ReceivableProtocol(StringIO().read, write)
+        handler = handler_cls(backend, [url_prefix(mat)], proto,
+                              http_req=req, advertise_refs=True)
+        handler.proto.write_pkt_line('# service=%s\n' % service)
+        handler.proto.write_pkt_line(None)
+        handler.handle()
+    else:
+        # non-smart fallback
+        # TODO: select_getanyfile() (see http-backend.c)
+        req.nocache()
+        req.respond(HTTP_OK, 'text/plain')
+        logger.info('Emulating dumb info/refs')
+        repo = get_repo(backend, mat)
+        refs = repo.get_refs()
+        for name in sorted(refs.iterkeys()):
+            # get_refs() includes HEAD as a special case, but we don't want to
+            # advertise it
+            if name == 'HEAD':
+                continue
+            sha = refs[name]
+            o = repo[sha]
+            if not o:
+                continue
+            yield '%s\t%s\n' % (sha, name)
+            peeled_sha = repo.get_peeled(name)
+            if peeled_sha != sha:
+                yield '%s\t%s^{}\n' % (peeled_sha, name)
+
+
+def get_info_packs(req, backend, mat):
+    req.nocache()
+    req.respond(HTTP_OK, 'text/plain')
+    logger.info('Emulating dumb info/packs')
+    for pack in get_repo(backend, mat).object_store.packs:
+        yield 'P pack-%s.pack\n' % pack.name()
+
+
+class _LengthLimitedFile(object):
+    """Wrapper class to limit the length of reads from a file-like object.
+
+    This is used to ensure EOF is read from the wsgi.input object once
+    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
+
+    def read(self, size=-1):
+        if self._bytes_avail <= 0:
+            return ''
+        if size == -1 or size > self._bytes_avail:
+            size = self._bytes_avail
+        self._bytes_avail -= size
+        return self._input.read(size)
+
+    # TODO: support more methods as necessary
+
+
+def handle_service_request(req, backend, mat):
+    service = mat.group().lstrip('/')
+    logger.info('Handling service request for %s', service)
+    handler_cls = req.handlers.get(service, None)
+    if handler_cls is None:
+        yield req.forbidden('Unsupported service %s' % service)
+        return
+    req.nocache()
+    write = req.respond(HTTP_OK, 'application/x-%s-result' % service)
+    proto = ReceivableProtocol(req.environ['wsgi.input'].read, write)
+    handler = handler_cls(backend, [url_prefix(mat)], proto, http_req=req)
+    handler.handle()
+
+
+class HTTPGitRequest(object):
+    """Class encapsulating the state of a single git HTTP request.
+
+    :ivar environ: the WSGI environment for the request.
+    """
+
+    def __init__(self, environ, start_response, dumb=False, handlers=None):
+        self.environ = environ
+        self.dumb = dumb
+        self.handlers = handlers
+        self._start_response = start_response
+        self._cache_headers = []
+        self._headers = []
+
+    def add_header(self, name, value):
+        """Add a header to the response."""
+        self._headers.append((name, value))
+
+    def respond(self, status=HTTP_OK, content_type=None, headers=None):
+        """Begin a response with the given status and other headers."""
+        if headers:
+            self._headers.extend(headers)
+        if content_type:
+            self._headers.append(('Content-Type', content_type))
+        self._headers.extend(self._cache_headers)
+
+        return self._start_response(status, self._headers)
+
+    def not_found(self, message):
+        """Begin a HTTP 404 response and return the text of a message."""
+        self._cache_headers = []
+        logger.info('Not found: %s', message)
+        self.respond(HTTP_NOT_FOUND, 'text/plain')
+        return message
+
+    def forbidden(self, message):
+        """Begin a HTTP 403 response and return the text of a message."""
+        self._cache_headers = []
+        logger.info('Forbidden: %s', message)
+        self.respond(HTTP_FORBIDDEN, 'text/plain')
+        return message
+
+    def error(self, message):
+        """Begin a HTTP 500 response and return the text of a message."""
+        self._cache_headers = []
+        logger.error('Error: %s', message)
+        self.respond(HTTP_ERROR, 'text/plain')
+        return message
+
+    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'),
+          ]
+
+    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'),
+          ]
+
+
+class HTTPGitApplication(object):
+    """Class encapsulating the state of a git WSGI application.
+
+    :ivar backend: the Backend object backing this application
+    """
+
+    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,
+    }
+
+    def __init__(self, backend, dumb=False, handlers=None):
+        self.backend = backend
+        self.dumb = dumb
+        self.handlers = dict(DEFAULT_HANDLERS)
+        if handlers is not None:
+            self.handlers.update(handlers)
+
+    def __call__(self, environ, start_response):
+        path = environ['PATH_INFO']
+        method = environ['REQUEST_METHOD']
+        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():
+            if smethod != method:
+                continue
+            mat = spath.search(path)
+            if mat:
+                handler = self.services[smethod, spath]
+                break
+        if handler is None:
+            return req.not_found('Sorry, that method is not supported')
+        return handler(req, self.backend, mat)
+
+
+class GunzipFilter(object):
+    """WSGI middleware that unzips gzip-encoded requests before
+    passing on to the underlying application.
+    """
+
+    def __init__(self, application):
+        self.app = application
+
+    def __call__(self, environ, start_response):
+        if environ.get('HTTP_CONTENT_ENCODING', '') == 'gzip':
+            # Note, we decompress everything in wsgi.input
+            # so that anything further in the chain sees
+            # a regular stream, and all relevant HTTP headers
+            # are updated
+            zlength = int(environ.get('CONTENT_LENGTH', '0'))
+            consumer = GzipConsumer()
+            consumer.feed(environ['wsgi.input'].read(zlength))
+            buf = consumer.close()
+            environ.pop('HTTP_CONTENT_ENCODING')
+
+            environ['CONTENT_LENGTH'] = str(buf.tell())
+            buf.seek(0)
+            environ['wsgi.input'] = buf
+
+        return self.app(environ, start_response)
+
+
+class LimitedInputFilter(object):
+    """WSGI middleware that limits the input length of a request to that
+    specified in Content-Length.
+    """
+
+    def __init__(self, application):
+        self.app = application
+
+    def __call__(self, environ, start_response):
+        # This is not necessary if this app is run from a conforming WSGI
+        # server. Unfortunately, there's no way to tell that at this point.
+        # TODO: git may used HTTP/1.1 chunked encoding instead of specifying
+        # content-length
+        content_length = environ.get('CONTENT_LENGTH', '')
+        if content_length:
+            input = environ['wsgi.input']
+            environ['wsgi.input'] = _LengthLimitedFile(input,
+                                                       int(content_length))
+
+        return self.app(environ, start_response)
+
+
+def make_wsgi_chain(backend, dumb=False, handlers=None):
+    """Factory function to create an instance of HTTPGitApplication,
+    correctly wrapped with needed middleware.
+    """
+    app = HTTPGitApplication(backend, dumb, handlers)
+    wrapped_app = GunzipFilter(LimitedInputFilter(app))
+    return wrapped_app
+
+
+# The reference server implementation is based on wsgiref, which is not
+# distributed with python 2.4. If wsgiref is not present, users will not be able
+# to use the HTTP server without a little extra work.
+try:
+    from wsgiref.simple_server import (
+        WSGIRequestHandler,
+        make_server,
+        )
+
+    class HTTPGitRequestHandler(WSGIRequestHandler):
+        """Handler that uses dulwich's logger for logging exceptions."""
+
+        def log_exception(self, exc_info):
+            logger.exception('Exception happened during processing of request',
+                             exc_info=exc_info)
+
+        def log_message(self, format, *args):
+            logger.info(format, *args)
+
+        def log_error(self, *args):
+            logger.error(*args)
+
+
+    def main(argv=sys.argv):
+        """Entry point for starting an HTTP git server."""
+        if len(argv) > 1:
+            gitdir = argv[1]
+        else:
+            gitdir = os.getcwd()
+
+        # TODO: allow serving on other addresses/ports via command-line flag
+        listen_addr=''
+        port = 8000
+
+        log_utils.default_logging_config()
+        backend = DictBackend({'/': Repo(gitdir)})
+        app = make_wsgi_chain(backend)
+        server = make_server(listen_addr, port, app,
+                             handler_class=HTTPGitRequestHandler)
+        logger.info('Listening for HTTP connections on %s:%d', listen_addr,
+                    port)
+        server.serve_forever()
+
+except ImportError:
+    # No wsgiref found; don't provide the reference functionality, but leave the
+    # rest of the WSGI-based implementation.
+    def main(argv=sys.argv):
+        """Stub entry point for failing to start a server without wsgiref."""
+        sys.stderr.write('Sorry, the wsgiref module is required for dul-web.\n')
+        sys.exit(1)
diff --git a/dulwich/web/paster.py b/dulwich/web/paster.py
new file mode 100644
index 0000000..4390690
--- /dev/null
+++ b/dulwich/web/paster.py
@@ -0,0 +1,148 @@
+# paster.py -- WSGI smart-http server
+# Copyright (C) 2011 David Blewett <david@xxxxxxxxxxxxxxxx>
+#
+# 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
+# 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.
+
+
+"""Factory functions for integrating the WSGI application into Paster."""
+
+import os
+
+from dulwich import log_utils
+from dulwich.errors import NotGitRepository
+from dulwich.repo import Repo
+from dulwich.server import DictBackend
+from dulwich.web import (
+    GunzipFilter,
+    HTTPGitApplication,
+    LimitedInputFilter,
+)
+
+logger = log_utils.getLogger(__name__)
+
+
+def make_app(global_config, **local_conf):
+    """Factory function for a Paster WSGI app
+
+    :param append_git: each served git repo have .git appended to its
+        served URL.
+    :param serve_dirs: list of parent paths to check for sub-directories
+        to serve.
+
+    :returns: An instance of dulwich.web.HTTPGitApplication
+
+    Two options to serve: serve_dirs and individual URL path to operating
+    system path mappings.
+    Example::
+
+        File-system layout:
+            +-/var/lib/git
+            |-foo
+            |-bar
+            `-baz
+
+            +-/home/git
+            |-bing
+            `-bang
+
+        paster.ini:
+            [app:main]
+            use = egg:dulwich
+            append_git = True
+            serve_dirs =
+                /var/lib/git
+                /home/git
+            blerg = /home/dannyboy/src/blerg
+
+    Will result in the following being served::
+
+        /foo.git   => /var/lib/git/foo
+        /bar.git   => /var/lib/git/bar
+        /baz.git   => /var/lib/git/baz
+        /bing.git  => /home/git/bing
+        /bang.git  => /home/git/bang
+        /blerg.git => /home/dannyboy/src/blerg
+
+    NOTE: The last name definition wins. Whatever directory in serve_dirs is
+          last, or the last explicit mapping for the same name is what will
+          be mapped.
+    """
+    from paste.deploy.converters import asbool
+    from paste.deploy.converters import aslist
+    repos = {}
+    append_git = asbool(local_conf.pop('append_git', False))
+    serve_dirs = aslist(local_conf.pop('serve_dirs', None))
+    log_utils.default_logging_config()
+
+    def add_repo(mapping, path, gitdir):
+        try:
+            mapping[path] = Repo(gitdir)
+        except NotGitRepository:
+            logger.error('Not a git repository, cannot serve: "%s".',
+                         gitdir)
+
+    if not local_conf and not serve_dirs:
+        add_repo(repos, '/', os.getcwd())
+    else:
+        if serve_dirs:
+            for top_dir in serve_dirs:
+                if not os.path.isdir(top_dir):
+                    logger.error('Not a directory, cannot serve: "%s".',
+                                 top_dir)
+
+                for d in os.listdir(top_dir):
+                    repo_path = '/'.join(('', d))
+                    gitdir = os.path.join(top_dir, d)
+                    add_repo(repos, repo_path, gitdir)
+
+        for repo_name, gitdir in local_conf.items():
+            repo_path = '/'.join(('', repo_name))
+            add_repo(repos, repo_path, gitdir)
+
+    if not repos:
+        msg = 'No repositories to serve, check the ini file: "%s".'
+        logger.error(msg, global_config['__file__'])
+        raise IndexError(msg % global_config['__file__'])
+
+    if append_git:
+        new_repos = {}
+        for rpath, repo in repos.items():
+            if rpath.endswith('.git'):
+                # Don't be redundant...
+                new_repos[rpath] = repo
+                logger.debug('Not renaming, already ends in .git: "%s".',
+                             rpath)
+            else:
+                new_repos['.'.join((rpath, 'git'))] = repo
+        backend = DictBackend(new_repos)
+    else:
+        backend = DictBackend(repos)
+    return HTTPGitApplication(backend)
+
+
+def make_gzip_filter(global_config):
+    """Factory function to wrap a given WSGI application in a GunzipFilter,
+    to transparently decode a gzip-encoded wsg.input stream.
+    """
+    return GunzipFilter
+
+
+def make_limit_input_filter(global_config):
+    """Factory function to wrap a given WSGI application in a
+    LimitedInputFilter, to ensure wsgi.input hits EOF when the Content-Length
+    is reached.
+    """
+    return LimitedInputFilter
diff --git a/setup.py b/setup.py
index 7472ee6..ee5deb7 100755
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@ class DulwichDistribution(Distribution):
         return not self.pure
 
     global_options = Distribution.global_options + [
-        ('pure', None, 
+        ('pure', None,
             "use pure (slower) Python code instead of C extensions")]
 
     pure = False
@@ -90,5 +90,14 @@ setup(name='dulwich',
               include_dirs=include_dirs),
           ],
       distclass=DulwichDistribution,
+      # If these are modified, the PasterFactoryTests.entry_points
+      # values need to be updated as well.
+      entry_points={
+          'paste.app_factory': 'main=dulwich.web.paster:make_app',
+          'paste.filter_factory': [
+              'gzip=dulwich.web.paster:make_gzip_filter',
+              'limitinput=dulwich.web.paster:make_limit_input_filter',
+            ]
+          },
       **setup_kwargs
       )

Follow ups

References