changeset 2510:cbff06469488

merge with crew.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Tue, 27 Jun 2006 00:13:44 -0700
parents 6350b01d173f (diff) 37655f10ced6 (current diff)
children 041d8f0a8437
files mercurial/commands.py
diffstat 8 files changed, 284 insertions(+), 99 deletions(-) [+]
line wrap: on
line diff
--- a/hgweb.cgi
+++ b/hgweb.cgi
@@ -6,7 +6,11 @@ import cgitb, os, sys
 cgitb.enable()
 
 # sys.path.insert(0, "/path/to/python/lib") # if not a system-wide install
-from mercurial import hgweb
+from mercurial.hgweb.hgweb_mod import hgweb
+from mercurial.hgweb.request import wsgiapplication
+import mercurial.hgweb.wsgicgi as wsgicgi
 
-h = hgweb.hgweb("/path/to/repo", "repository name")
-h.run()
+def make_web_app():
+    return hgweb("/path/to/repo", "repository name")
+
+wsgicgi.launch(wsgiapplication(make_web_app))
--- a/hgwebdir.cgi
+++ b/hgwebdir.cgi
@@ -6,7 +6,9 @@ import cgitb, sys
 cgitb.enable()
 
 # sys.path.insert(0, "/path/to/python/lib") # if not a system-wide install
-from mercurial import hgweb
+from mercurial.hgweb.hgwebdir_mod import hgwebdir
+from mercurial.hgweb.request import wsgiapplication
+import mercurial.hgweb.wsgicgi as wsgicgi
 
 # The config file looks like this.  You can have paths to individual
 # repos, collections of repos in a directory tree, or both.
@@ -27,5 +29,7 @@ from mercurial import hgweb
 # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
 # or use a dictionary with entries like 'virtual/path': '/real/path'
 
-h = hgweb.hgwebdir("hgweb.config")
-h.run()
+def make_web_app():
+    return hgwebdir("hgweb.config")
+
+wsgicgi.launch(wsgiapplication(make_web_app))
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -12,7 +12,7 @@ demandload(globals(), "os re sys signal 
 demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo")
 demandload(globals(), "fnmatch mdiff random signal tempfile time")
 demandload(globals(), "traceback errno socket version struct atexit sets bz2")
-demandload(globals(), "archival changegroup")
+demandload(globals(), "archival cStringIO changegroup email.Parser")
 demandload(globals(), "hgweb.server sshserver")
 
 class UnknownCommand(Exception):
@@ -1719,11 +1719,15 @@ def import_(ui, repo, patch1, *patches, 
     If there are outstanding changes in the working directory, import
     will abort unless given the -f flag.
 
-    If a patch looks like a mail message (its first line starts with
-    "From " or looks like an RFC822 header), it will not be applied
-    unless the -f option is used.  The importer neither parses nor
-    discards mail headers, so use -f only to override the "mailness"
-    safety check, not to import a real mail message.
+    You can import a patch straight from a mail message.  Even patches
+    as attachments work (body part must be type text/plain or
+    text/x-patch to be used).  Sender and subject line of email
+    message are used as default committer and commit message.  Any
+    text/plain body part before first diff is added to commit message.
+
+    If imported patch was generated by hg export, user and description
+    from patch override values from message headers and body.  Values
+    given on command line with -m and -u override these.
 
     To read a patch from standard input, use patch name "-".
     """
@@ -1739,79 +1743,93 @@ def import_(ui, repo, patch1, *patches, 
 
     # attempt to detect the start of a patch
     # (this heuristic is borrowed from quilt)
-    diffre = re.compile(r'(?:Index:[ \t]|diff[ \t]|RCS file: |' +
+    diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
                         'retrieving revision [0-9]+(\.[0-9]+)*$|' +
-                        '(---|\*\*\*)[ \t])')
+                        '(---|\*\*\*)[ \t])', re.MULTILINE)
 
     for patch in patches:
         pf = os.path.join(d, patch)
 
-        message = []
+        message = None
         user = None
         date = None
         hgpatch = False
+
+        p = email.Parser.Parser()
         if pf == '-':
-            f = sys.stdin
-            fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
-            pf = tmpname
-            tmpfp = os.fdopen(fd, 'w')
+            msg = p.parse(sys.stdin)
             ui.status(_("applying patch from stdin\n"))
         else:
-            f = open(pf)
-            tmpfp, tmpname = None, None
+            msg = p.parse(file(pf))
             ui.status(_("applying %s\n") % patch)
+
+        fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
+        tmpfp = os.fdopen(fd, 'w')
         try:
-            while True:
-                line = f.readline()
-                if not line: break
-                if tmpfp: tmpfp.write(line)
-                line = line.rstrip()
-                if (not message and not hgpatch and
-                       mailre.match(line) and not opts['force']):
-                    if len(line) > 35:
-                        line = line[:32] + '...'
-                    raise util.Abort(_('first line looks like a '
-                                       'mail header: ') + line)
-                if diffre.match(line):
+            message = msg['Subject']
+            if message:
+                message = message.replace('\n\t', ' ')
+                ui.debug('Subject: %s\n' % message)
+            user = msg['From']
+            if user:
+                ui.debug('From: %s\n' % user)
+            diffs_seen = 0
+            ok_types = ('text/plain', 'text/x-patch')
+            for part in msg.walk():
+                content_type = part.get_content_type()
+                ui.debug('Content-Type: %s\n' % content_type)
+                if content_type not in ok_types:
+                    continue
+                payload = part.get_payload(decode=True)
+                m = diffre.search(payload)
+                if m:
+                    ui.debug(_('found patch at byte %d\n') % m.start(0))
+                    diffs_seen += 1
+                    hgpatch = False
+                    fp = cStringIO.StringIO()
+                    for line in payload[:m.start(0)].splitlines():
+                        if line.startswith('# HG changeset patch'):
+                            ui.debug(_('patch generated by hg export\n'))
+                            hgpatch = True
+                        elif hgpatch:
+                            if line.startswith('# User '):
+                                user = line[7:]
+                                ui.debug('From: %s\n' % user)
+                            elif line.startswith("# Date "):
+                                date = line[7:]
+                        if not line.startswith('# '):
+                            fp.write(line)
+                            fp.write('\n')
+                            hgpatch = False
+                    message = fp.getvalue() or message
                     if tmpfp:
-                        for chunk in util.filechunkiter(f):
-                            tmpfp.write(chunk)
-                    break
-                elif hgpatch:
-                    # parse values when importing the result of an hg export
-                    if line.startswith("# User "):
-                        user = line[7:]
-                        ui.debug(_('User: %s\n') % user)
-                    elif line.startswith("# Date "):
-                        date = line[7:]
-                    elif not line.startswith("# ") and line:
-                        message.append(line)
-                        hgpatch = False
-                elif line == '# HG changeset patch':
-                    hgpatch = True
-                    message = []       # We may have collected garbage
-                elif message or line:
-                    message.append(line)
+                        tmpfp.write(payload)
+                        if not payload.endswith('\n'):
+                            tmpfp.write('\n')
+                elif not diffs_seen and message and content_type == 'text/plain':
+                    message += '\n' + payload
 
             if opts['message']:
                 # pickup the cmdline msg
                 message = opts['message']
             elif message:
                 # pickup the patch msg
-                message = '\n'.join(message).rstrip()
+                message = message.strip()
             else:
                 # launch the editor
                 message = None
             ui.debug(_('message:\n%s\n') % message)
 
-            if tmpfp: tmpfp.close()
-            files = util.patch(strip, pf, ui)
-
+            tmpfp.close()
+            if not diffs_seen:
+                raise util.Abort(_('no diffs found'))
+
+            files = util.patch(strip, tmpname, ui)
             if len(files) > 0:
                 addremove_lock(ui, repo, files, {})
             repo.commit(files, message, user, date)
         finally:
-            if tmpname: os.unlink(tmpname)
+            os.unlink(tmpname)
 
 def incoming(ui, repo, source="default", **opts):
     """show new changesets found in source
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -12,7 +12,6 @@ import mimetypes
 from mercurial.demandload import demandload
 demandload(globals(), "re zlib ConfigParser cStringIO sys tempfile")
 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
-demandload(globals(), "mercurial.hgweb.request:hgrequest")
 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
 from mercurial.node import *
 from mercurial.i18n import gettext as _
@@ -651,7 +650,7 @@ class hgweb(object):
             raise Exception("suspicious path")
         return p
 
-    def run(self, req=hgrequest()):
+    def run(self, req):
         def header(**map):
             yield self.t("header", **map)
 
@@ -724,7 +723,6 @@ class hgweb(object):
             method(req)
         else:
             req.write(self.t("error"))
-        req.done()
 
     def do_changelog(self, req):
         hi = self.repo.changelog.count() - 1
--- a/mercurial/hgweb/hgwebdir_mod.py
+++ b/mercurial/hgweb/hgwebdir_mod.py
@@ -11,7 +11,6 @@ from mercurial.demandload import demandl
 demandload(globals(), "ConfigParser")
 demandload(globals(), "mercurial:ui,hg,util,templater")
 demandload(globals(), "mercurial.hgweb.hgweb_mod:hgweb")
-demandload(globals(), "mercurial.hgweb.request:hgrequest")
 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
 from mercurial.i18n import gettext as _
 
@@ -47,7 +46,7 @@ class hgwebdir(object):
                         self.repos.append((name.lstrip(os.sep), repo))
             self.repos.sort()
 
-    def run(self, req=hgrequest()):
+    def run(self, req):
         def header(**map):
             yield tmpl("header", **map)
 
--- a/mercurial/hgweb/request.py
+++ b/mercurial/hgweb/request.py
@@ -10,40 +10,73 @@ from mercurial.demandload import demandl
 demandload(globals(), "socket sys cgi os errno")
 from mercurial.i18n import gettext as _
 
-class hgrequest(object):
-    def __init__(self, inp=None, out=None, env=None):
-        self.inp = inp or sys.stdin
-        self.out = out or sys.stdout
-        self.env = env or os.environ
+class wsgiapplication(object):
+    def __init__(self, destmaker):
+        self.destmaker = destmaker
+
+    def __call__(self, wsgienv, start_response):
+        return _wsgirequest(self.destmaker(), wsgienv, start_response)
+
+class _wsgioutputfile(object):
+    def __init__(self, request):
+        self.request = request
+
+    def write(self, data):
+        self.request.write(data)
+    def writelines(self, lines):
+        for line in lines:
+            self.write(line)
+    def flush(self):
+        return None
+    def close(self):
+        return None
+
+class _wsgirequest(object):
+    def __init__(self, destination, wsgienv, start_response):
+        version = wsgienv['wsgi.version']
+        if (version < (1,0)) or (version >= (2, 0)):
+            raise RuntimeError("Unknown and unsupported WSGI version %d.%d" \
+                               % version)
+        self.inp = wsgienv['wsgi.input']
+        self.out = _wsgioutputfile(self)
+        self.server_write = None
+        self.err = wsgienv['wsgi.errors']
+        self.threaded = wsgienv['wsgi.multithread']
+        self.multiprocess = wsgienv['wsgi.multiprocess']
+        self.run_once = wsgienv['wsgi.run_once']
+        self.env = wsgienv
         self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
-        self.will_close = True
+        self.start_response = start_response
+        self.headers = []
+        destination.run(self)
+
+    def __iter__(self):
+        return iter([])
 
     def read(self, count=-1):
         return self.inp.read(count)
 
     def write(self, *things):
+        if self.server_write is None:
+            if not self.headers:
+                self.header()
+            self.server_write = self.start_response('200 Script output follows',
+                                                    self.headers)
+            self.start_response = None
+            self.headers = None
         for thing in things:
             if hasattr(thing, "__iter__"):
                 for part in thing:
                     self.write(part)
             else:
                 try:
-                    self.out.write(str(thing))
+                    self.server_write(str(thing))
                 except socket.error, inst:
                     if inst[0] != errno.ECONNRESET:
                         raise
 
-    def done(self):
-        if self.will_close:
-            self.inp.close()
-            self.out.close()
-        else:
-            self.out.flush()
-
     def header(self, headers=[('Content-type','text/html')]):
-        for header in headers:
-            self.out.write("%s: %s\r\n" % header)
-        self.out.write("\r\n")
+        self.headers.extend(headers)
 
     def httphdr(self, type, filename=None, length=0, headers={}):
         headers = headers.items()
@@ -51,12 +84,6 @@ class hgrequest(object):
         if filename:
             headers.append(('Content-disposition', 'attachment; filename=%s' %
                             filename))
-        # we do not yet support http 1.1 chunked transfer, so we have
-        # to force connection to close if content-length not known
         if length:
             headers.append(('Content-length', str(length)))
-            self.will_close = False
-        else:
-            headers.append(('Connection', 'close'))
-            self.will_close = True
         self.header(headers)
--- a/mercurial/hgweb/server.py
+++ b/mercurial/hgweb/server.py
@@ -10,7 +10,7 @@ from mercurial.demandload import demandl
 import os, sys, errno
 demandload(globals(), "urllib BaseHTTPServer socket SocketServer")
 demandload(globals(), "mercurial:ui,hg,util,templater")
-demandload(globals(), "hgweb_mod:hgweb hgwebdir_mod:hgwebdir request:hgrequest")
+demandload(globals(), "hgweb_mod:hgweb hgwebdir_mod:hgwebdir request:wsgiapplication")
 from mercurial.i18n import gettext as _
 
 def _splitURI(uri):
@@ -25,6 +25,17 @@ def _splitURI(uri):
         path, query = uri, ''
     return urllib.unquote(path), query
 
+class _error_logger(object):
+    def __init__(self, handler):
+        self.handler = handler
+    def flush(self):
+        pass
+    def write(str):
+        self.writelines(str.split('\n'))
+    def writelines(seq):
+        for msg in seq:
+            self.handler.log_error("HG error:  %s", msg)
+
 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
     def __init__(self, *args, **kargs):
         self.protocol_version = 'HTTP/1.1'
@@ -76,17 +87,72 @@ class _hgwebhandler(object, BaseHTTPServ
         length = self.headers.getheader('content-length')
         if length:
             env['CONTENT_LENGTH'] = length
-        accept = []
-        for line in self.headers.getallmatchingheaders('accept'):
-            if line[:1] in "\t\n\r ":
-                accept.append(line.strip())
-            else:
-                accept = accept + line[7:].split(',')
-        env['HTTP_ACCEPT'] = ','.join(accept)
+        for header in [h for h in self.headers.keys() \
+                       if h not in ('content-type', 'content-length')]:
+            hkey = 'HTTP_' + header.replace('-', '_').upper()
+            hval = self.headers.getheader(header)
+            hval = hval.replace('\n', '').strip()
+            if hval:
+                env[hkey] = hval
+        env['SERVER_PROTOCOL'] = self.request_version
+        env['wsgi.version'] = (1, 0)
+        env['wsgi.url_scheme'] = 'http'
+        env['wsgi.input'] = self.rfile
+        env['wsgi.errors'] = _error_logger(self)
+        env['wsgi.multithread'] = isinstance(self.server,
+                                             SocketServer.ThreadingMixIn)
+        env['wsgi.multiprocess'] = isinstance(self.server,
+                                              SocketServer.ForkingMixIn)
+        env['wsgi.run_once'] = 0
+
+        self.close_connection = True
+        self.saved_status = None
+        self.saved_headers = []
+        self.sent_headers = False
+        self.length = None
+        req = self.server.reqmaker(env, self._start_response)
+        for data in req:
+            if data:
+                self._write(data)
 
-        req = hgrequest(self.rfile, self.wfile, env)
-        self.send_response(200, "Script output follows")
-        self.close_connection = self.server.make_and_run_handler(req)
+    def send_headers(self):
+        if not self.saved_status:
+            raise AssertionError("Sending headers before start_response() called")
+        saved_status = self.saved_status.split(None, 1)
+        saved_status[0] = int(saved_status[0])
+        self.send_response(*saved_status)
+        should_close = True
+        for h in self.saved_headers:
+            self.send_header(*h)
+            if h[0].lower() == 'content-length':
+                should_close = False
+                self.length = int(h[1])
+        if should_close:
+            self.send_header('Connection', 'close')
+        self.close_connection = should_close
+        self.end_headers()
+        self.sent_headers = True
+
+    def _start_response(self, http_status, headers, exc_info=None):
+        code, msg = http_status.split(None, 1)
+        code = int(code)
+        self.saved_status = http_status
+        bad_headers = ('connection', 'transfer-encoding')
+        self.saved_headers = [ h for h in headers \
+                               if h[0].lower() not in bad_headers ]
+        return self._write
+
+    def _write(self, data):
+        if not self.saved_status:
+            raise AssertionError("data written before start_response() called")
+        elif not self.sent_headers:
+            self.send_headers()
+        if self.length is not None:
+            if len(data) > self.length:
+                raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
+            self.length = self.length - len(data)
+        self.wfile.write(data)
+        self.wfile.flush()
 
 def create_server(ui, repo):
     use_threads = True
@@ -126,8 +192,9 @@ def create_server(ui, repo):
             self.webdir_conf = webdir_conf
             self.webdirmaker = hgwebdir
             self.repoviewmaker = hgweb
+            self.reqmaker = wsgiapplication(self.make_handler)
 
-        def make_and_run_handler(self, req):
+        def make_handler(self):
             if self.webdir_conf:
                 hgwebobj = self.webdirmaker(self.webdir_conf)
             elif self.repo is not None:
@@ -135,8 +202,7 @@ def create_server(ui, repo):
                                                              repo.origroot))
             else:
                 raise hg.RepoError(_('no repo found'))
-            hgwebobj.run(req)
-            return req.will_close
+            return hgwebobj
 
     class IPv6HTTPServer(MercurialHTTPServer):
         address_family = getattr(socket, 'AF_INET6', None)
@@ -144,7 +210,7 @@ def create_server(ui, repo):
         def __init__(self, *args, **kwargs):
             if self.address_family is None:
                 raise hg.RepoError(_('IPv6 not available on this system'))
-            super(IPv6HTTPServer, self).__init__(*args, **kargs)
+            super(IPv6HTTPServer, self).__init__(*args, **kwargs)
 
     if use_ipv6:
         return IPv6HTTPServer((address, port), _hgwebhandler)
new file mode 100644
--- /dev/null
+++ b/mercurial/hgweb/wsgicgi.py
@@ -0,0 +1,69 @@
+# hgweb/wsgicgi.py - CGI->WSGI translator
+#
+# Copyright 2006 Eric Hopper <hopper@omnifarious.org>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+#
+# This was originally copied from the public domain code at
+# http://www.python.org/dev/peps/pep-0333/#the-server-gateway-side
+
+import os, sys
+
+def launch(application):
+
+    environ = dict(os.environ.items())
+    environ['wsgi.input']        = sys.stdin
+    environ['wsgi.errors']       = sys.stderr
+    environ['wsgi.version']      = (1,0)
+    environ['wsgi.multithread']  = False
+    environ['wsgi.multiprocess'] = True
+    environ['wsgi.run_once']    = True
+
+    if environ.get('HTTPS','off') in ('on','1'):
+        environ['wsgi.url_scheme'] = 'https'
+    else:
+        environ['wsgi.url_scheme'] = 'http'
+
+    headers_set = []
+    headers_sent = []
+
+    def write(data):
+        if not headers_set:
+             raise AssertionError("write() before start_response()")
+
+        elif not headers_sent:
+             # Before the first output, send the stored headers
+             status, response_headers = headers_sent[:] = headers_set
+             sys.stdout.write('Status: %s\r\n' % status)
+             for header in response_headers:
+                 sys.stdout.write('%s: %s\r\n' % header)
+             sys.stdout.write('\r\n')
+
+        sys.stdout.write(data)
+        sys.stdout.flush()
+
+    def start_response(status,response_headers,exc_info=None):
+        if exc_info:
+            try:
+                if headers_sent:
+                    # Re-raise original exception if headers sent
+                    raise exc_info[0], exc_info[1], exc_info[2]
+            finally:
+                exc_info = None     # avoid dangling circular ref
+        elif headers_set:
+            raise AssertionError("Headers already set!")
+
+        headers_set[:] = [status,response_headers]
+        return write
+
+    result = application(environ, start_response)
+    try:
+        for data in result:
+            if data:    # don't send headers until body appears
+                write(data)
+        if not headers_sent:
+            write('')   # send headers now if body was empty
+    finally:
+        if hasattr(result,'close'):
+            result.close()