merge with crew.
--- 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()