dulwich-users team mailing list archive
-
dulwich-users team
-
Mailing list archive
-
Message #00684
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
-
[PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-19
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-19
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-20
-
Re: [PATCH] Unified gzip/paster patch
From: Jelmer Vernooij, 2011-03-20
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-20
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-20
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-20
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-03-21
-
Re: [PATCH] Unified gzip/paster patch
From: Jelmer Vernooij, 2011-03-25
-
Re: [PATCH] Unified gzip/paster patch
From: David Blewett, 2011-04-04
-
Re: [PATCH] Unified gzip/paster patch
From: Jelmer Vernooij, 2011-04-16
-
Re: [PATCH] Unified gzip/paster patch
From: Jelmer Vernooij, 2011-04-24