Mercurial > kallithea
diff rhodecode/lib/middleware/pygrack.py @ 2382:034e4fe1ebb2 beta
changed dulwich git interface to gitweb + subprocessio
author | Marcin Kuzminski <marcin@python-works.com> |
---|---|
date | Mon, 04 Jun 2012 02:56:09 +0200 |
parents | |
children | 378b0247e938 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/middleware/pygrack.py Mon Jun 04 02:56:09 2012 +0200 @@ -0,0 +1,181 @@ +import os +import socket +import logging +import subprocess + +from webob import Request, Response, exc + +from rhodecode.lib import subprocessio + +log = logging.getLogger(__name__) + + +class FileWrapper(object): + + def __init__(self, fd, content_length): + self.fd = fd + self.content_length = content_length + self.remain = content_length + + def read(self, size): + if size <= self.remain: + try: + data = self.fd.read(size) + except socket.error: + raise IOError(self) + self.remain -= size + elif self.remain: + data = self.fd.read(self.remain) + self.remain = 0 + else: + data = None + return data + + def __repr__(self): + return '<FileWrapper %s len: %s, read: %s>' % ( + self.fd, self.content_length, self.content_length - self.remain + ) + + +class GitRepository(object): + git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) + commands = ['git-upload-pack', 'git-receive-pack'] + + def __init__(self, repo_name, content_path): + files = set([f.lower() for f in os.listdir(content_path)]) + if not (self.git_folder_signature.intersection(files) + == self.git_folder_signature): + raise OSError('%s missing git signature' % content_path) + self.content_path = content_path + self.valid_accepts = ['application/x-%s-result' % + c for c in self.commands] + self.repo_name = repo_name + + def _get_fixedpath(self, path): + """ + Small fix for repo_path + + :param path: + :type path: + """ + return path.split(self.repo_name, 1)[-1].strip('/') + + def inforefs(self, request, environ): + """ + WSGI Response producer for HTTP GET Git Smart + HTTP /info/refs request. + """ + + git_command = request.GET['service'] + if git_command not in self.commands: + log.debug('command %s not allowed' % git_command) + return exc.HTTPMethodNotAllowed() + + # note to self: + # please, resist the urge to add '\n' to git capture and increment + # line count by 1. + # The code in Git client not only does NOT need '\n', but actually + # blows up if you sprinkle "flush" (0000) as "0001\n". + # It reads binary, per number of bytes specified. + # if you do add '\n' as part of data, count it. + smart_server_advert = '# service=%s' % git_command + try: + out = subprocessio.SubprocessIOChunker( + r'git %s --stateless-rpc --advertise-refs "%s"' % ( + git_command[4:], self.content_path), + starting_values=[ + str(hex(len(smart_server_advert) + 4)[2:] + .rjust(4, '0') + smart_server_advert + '0000') + ] + ) + except EnvironmentError, e: + log.exception(e) + raise exc.HTTPExpectationFailed() + resp = Response() + resp.content_type = 'application/x-%s-advertisement' % str(git_command) + resp.app_iter = out + return resp + + def backend(self, request, environ): + """ + WSGI Response producer for HTTP POST Git Smart HTTP requests. + Reads commands and data from HTTP POST's body. + returns an iterator obj with contents of git command's + response to stdout + """ + git_command = self._get_fixedpath(request.path_info) + if git_command not in self.commands: + log.debug('command %s not allowed' % git_command) + return exc.HTTPMethodNotAllowed() + + if 'CONTENT_LENGTH' in environ: + inputstream = FileWrapper(environ['wsgi.input'], + request.content_length) + else: + inputstream = environ['wsgi.input'] + + try: + out = subprocessio.SubprocessIOChunker( + r'git %s --stateless-rpc "%s"' % (git_command[4:], + self.content_path), + inputstream=inputstream + ) + except EnvironmentError, e: + log.exception(e) + raise exc.HTTPExpectationFailed() + + if git_command in [u'git-receive-pack']: + # updating refs manually after each push. + # Needed for pre-1.7.0.4 git clients using regular HTTP mode. + subprocess.call(u'git --git-dir "%s" ' + 'update-server-info' % self.content_path, + shell=True) + + resp = Response() + resp.content_type = 'application/x-%s-result' % git_command.encode('utf8') + resp.app_iter = out + return resp + + def __call__(self, environ, start_response): + request = Request(environ) + _path = self._get_fixedpath(request.path_info) + if _path.startswith('info/refs'): + app = self.inforefs + elif [a for a in self.valid_accepts if a in request.accept]: + app = self.backend + try: + resp = app(request, environ) + except exc.HTTPException, e: + resp = e + log.exception(e) + except Exception, e: + log.exception(e) + resp = exc.HTTPInternalServerError() + return resp(environ, start_response) + + +class GitDirectory(object): + + def __init__(self, repo_root, repo_name): + repo_location = os.path.join(repo_root, repo_name) + if not os.path.isdir(repo_location): + raise OSError(repo_location) + + self.content_path = repo_location + self.repo_name = repo_name + self.repo_location = repo_location + + def __call__(self, environ, start_response): + content_path = self.content_path + try: + app = GitRepository(self.repo_name, content_path) + except (AssertionError, OSError): + if os.path.isdir(os.path.join(content_path, '.git')): + app = GitRepository(os.path.join(content_path, '.git')) + else: + return exc.HTTPNotFound()(environ, start_response) + return app(environ, start_response) + + +def make_wsgi_app(repo_name, repo_root): + return GitDirectory(repo_root, repo_name)