changeset 2531:7a90e0c77f43

Merge with crew.
author Thomas Arendsen Hein <thomas@intevation.de>
date Fri, 30 Jun 2006 21:35:28 +0200
parents d181845bdc51 (current diff) c1974f65d781 (diff)
children 8a8d9ada4528
files hgext/mq.py
diffstat 24 files changed, 684 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/macosx/Readme.html
+++ b/contrib/macosx/Readme.html
@@ -18,13 +18,10 @@
 <p class="p2"><br></p>
 <p class="p3">This is <i>not</i> a stand-alone version of Mercurial.</p>
 <p class="p2"><br></p>
-<p class="p3">To use it, you must have the “official unofficial” MacPython 2.4.1 installed.</p>
+<p class="p3">To use it, you must have the Universal MacPython 2.4.3 from <a href="http://www.python.org">www.python.org</a> installed.</p>
 <p class="p2"><br></p>
-<p class="p3">You can download MacPython 2.4.1 from here:</p>
-<p class="p4"><span class="s1"><a href="http://python.org/ftp/python/2.4.1/MacPython-OSX-2.4.1-1.dmg">http://python.org/ftp/python/2.4.1/MacPython-OSX-2.4.1-1.dmg</a></span></p>
-<p class="p2"><br></p>
-<p class="p3">For more information on MacPython, go here:</p>
-<p class="p4"><span class="s1"><a href="http://undefined.org/python/">http://undefined.org/python</a></span></p>
+<p class="p3">You can download MacPython 2.4.3 from here:</p>
+<p class="p4"><span class="s1"><a href="http://www.python.org/ftp/python/2.4.3/Universal-MacPython-2.4.3-2006-04-07.dmg">http://www.python.org/ftp/python/2.4.3/Universal-MacPython-2.4.3-2006-04-07.dmg</a></span></p>
 <p class="p2"><br></p>
 <p class="p1"><b>After you install</b></p>
 <p class="p2"><br></p>
--- a/contrib/macosx/Welcome.html
+++ b/contrib/macosx/Welcome.html
@@ -12,6 +12,6 @@
 <body>
 <p class="p1">This is a prepackaged release of <a href="http://www.selenic.com/mercurial">Mercurial</a> for Mac OS X.</p>
 <p class="p2"><br></p>
-<p class="p1">It is based on Mercurial 0.8.</p>
+<p class="p1">It is based on Mercurial 0.9.</p>
 </body>
 </html>
--- a/contrib/mercurial.el
+++ b/contrib/mercurial.el
@@ -653,7 +653,7 @@ The Mercurial mode user interface is bas
 you're already familiar with VC, the same keybindings and functions
 will generally work.
 
-Below is a list of many common SCM tasks.  In the list, `G/L'
+Below is a list of many common SCM tasks.  In the list, `G/L\'
 indicates whether a key binding is global (G) to a repository or local
 (L) to a file.  Many commands take a prefix argument.
 
@@ -682,6 +682,8 @@ Pull changes                          G 
 Update working directory after pull   G    C-c h u      hg-update
 See changes that can be pushed        G    C-c h .      hg-outgoing
 Push changes                          G    C-c h >      hg-push"
+  (unless vc-make-backup-files
+    (set (make-local-variable 'backup-inhibited) t))
   (run-hooks 'hg-mode-hook))
 
 (defun hg-find-file-hook ()
@@ -729,6 +731,8 @@ With a prefix argument, prompt for the p
       (goto-char 0)
       (cd (hg-root path)))
     (when update
+      (unless vc-make-backup-files
+	(set (make-local-variable 'backup-inhibited) t))
       (with-current-buffer buf
 	(hg-mode-line)))))
 
@@ -968,6 +972,7 @@ With a prefix argument, prompt for the p
       (cd (hg-root path)))
     (when update
       (with-current-buffer buf
+	(set (make-local-variable 'backup-inhibited) nil)
 	(hg-mode-line)))))
 
 (defun hg-incoming (&optional repo)
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -214,7 +214,6 @@ class queue:
                 return pp[0]
             if p1 in arevs:
                 return pp[1]
-            return None
         return pp[0]
 
     def mergepatch(self, repo, mergeq, series, wlock):
@@ -386,15 +385,21 @@ class queue:
             self.ui.write("Local changes found, refresh first\n")
             sys.exit(1)
     def new(self, repo, patch, msg=None, force=None):
-        if not force:
-            self.check_localchanges(repo)
+        commitfiles = []
+        (c, a, r, d, u) = repo.changes(None, None)
+        if c or a or d or r:
+            if not force:
+                raise util.Abort(_("Local changes found, refresh first"))
+            else:
+                commitfiles = c + a + r
         self.check_toppatch(repo)
         wlock = repo.wlock()
         insert = self.series_end()
         if msg:
-            n = repo.commit([], "[mq]: %s" % msg, force=True, wlock=wlock)
+            n = repo.commit(commitfiles, "[mq]: %s" % msg, force=True,
+                            wlock=wlock)
         else:
-            n = repo.commit([],
+            n = repo.commit(commitfiles,
                             "New patch: %s" % patch, force=True, wlock=wlock)
         if n == None:
             self.ui.warn("repo commit failed\n")
@@ -412,6 +417,8 @@ class queue:
         wlock = None
         r = self.qrepo()
         if r: r.add([patch])
+        if commitfiles:
+            self.refresh(repo, short=True)
 
     def strip(self, repo, rev, update=True, backup="all", wlock=None):
         def limitheads(chlog, stop):
--- 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/changelog.py
+++ b/mercurial/changelog.py
@@ -39,21 +39,10 @@ class changelog(revlog):
     def add(self, manifest, list, desc, transaction, p1=None, p2=None,
                   user=None, date=None):
         if date:
-            # validate explicit (probably user-specified) date and
-            # time zone offset. values must fit in signed 32 bits for
-            # current 32-bit linux runtimes. timezones go from UTC-12
-            # to UTC+14
-            try:
-                when, offset = map(int, date.split(' '))
-            except ValueError:
-                raise ValueError(_('invalid date: %r') % date)
-            if abs(when) > 0x7fffffff:
-                raise ValueError(_('date exceeds 32 bits: %d') % when)
-            if offset < -50400 or offset > 43200:
-                raise ValueError(_('impossible time zone offset: %d') % offset)
+            parseddate = "%d %d" % util.parsedate(date)
         else:
-            date = "%d %d" % util.makedate()
+            parseddate = "%d %d" % util.makedate()
         list.sort()
-        l = [hex(manifest), user, date] + list + ["", desc]
+        l = [hex(manifest), user, parseddate] + list + ["", desc]
         text = "\n".join(l)
         return self.addrevision(text, transaction, self.count(), p1, p2)
--- 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,16 @@ 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).  From and Subject headers of email
+    message are used as default committer and commit message.  All
+    text/plain body parts before first diff are 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 +1744,98 @@ 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()
+                    if message:
+                        fp.write(message)
+                        fp.write('\n')
+                    for line in payload[:m.start(0)].splitlines():
+                        if line.startswith('# HG changeset patch'):
+                            ui.debug(_('patch generated by hg export\n'))
+                            hgpatch = True
+                            # drop earlier commit message content
+                            fp.seek(0)
+                            fp.truncate()
+                        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')
+                    message = fp.getvalue()
                     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
@@ -1851,7 +1875,10 @@ def incoming(ui, repo, source="default",
                 # use the created uncompressed bundlerepo
                 other = bundlerepo.bundlerepository(ui, repo.root, fname)
 
-        o = other.changelog.nodesbetween(incoming)[0]
+        revs = None
+        if opts['rev']:
+            revs = [other.lookup(rev) for rev in opts['rev']]
+        o = other.changelog.nodesbetween(incoming, revs)[0]
         if opts['newest_first']:
             o.reverse()
         displayer = show_changeset(ui, other, opts)
@@ -2061,13 +2088,16 @@ def outgoing(ui, repo, dest=None, **opts
         ui.setconfig("ui", "ssh", opts['ssh'])
     if opts['remotecmd']:
         ui.setconfig("ui", "remotecmd", opts['remotecmd'])
+    revs = None
+    if opts['rev']:
+        revs = [repo.lookup(rev) for rev in opts['rev']]
 
     other = hg.repository(ui, dest)
     o = repo.findoutgoing(other, force=opts['force'])
     if not o:
         ui.status(_("no changes found\n"))
         return
-    o = repo.changelog.nodesbetween(o)[0]
+    o = repo.changelog.nodesbetween(o, revs)[0]
     if opts['newest_first']:
         o.reverse()
     displayer = show_changeset(ui, repo, opts)
@@ -2998,11 +3028,13 @@ table = {
           ('n', 'newest-first', None, _('show newest record first')),
           ('', 'bundle', '', _('file to store the bundles into')),
           ('p', 'patch', None, _('show patch')),
+          ('r', 'rev', [], _('a specific revision you would like to pull')),
           ('', 'template', '', _('display with template')),
           ('e', 'ssh', '', _('specify ssh command to use')),
           ('', 'remotecmd', '',
            _('specify hg command to run on the remote side'))],
-         _('hg incoming [-p] [-n] [-M] [--bundle FILENAME] [SOURCE]')),
+         _('hg incoming [-p] [-n] [-M] [-r REV]...'
+           '[--bundle FILENAME] [SOURCE]')),
     "^init": (init, [], _('hg init [DEST]')),
     "locate":
         (locate,
@@ -3040,12 +3072,13 @@ table = {
            _('run even when remote repository is unrelated')),
           ('p', 'patch', None, _('show patch')),
           ('', 'style', '', _('display using template map file')),
+          ('r', 'rev', [], _('a specific revision you would like to push')),
           ('n', 'newest-first', None, _('show newest record first')),
           ('', 'template', '', _('display with template')),
           ('e', 'ssh', '', _('specify ssh command to use')),
           ('', 'remotecmd', '',
            _('specify hg command to run on the remote side'))],
-         _('hg outgoing [-M] [-p] [-n] [DEST]')),
+         _('hg outgoing [-M] [-p] [-n] [-r REV]... [DEST]')),
     "^parents":
         (parents,
          [('b', 'branches', None, _('show branches')),
--- a/mercurial/hgweb/common.py
+++ b/mercurial/hgweb/common.py
@@ -17,7 +17,7 @@ def get_mtime(repo_path):
     else:
         return os.stat(hg_path).st_mtime
 
-def staticfile(directory, fname):
+def staticfile(directory, fname, req):
     """return a file inside directory with guessed content-type header
 
     fname always uses '/' as directory separator and isn't allowed to
@@ -36,7 +36,9 @@ def staticfile(directory, fname):
     try:
         os.stat(path)
         ct = mimetypes.guess_type(path)[0] or "text/plain"
-        return "Content-type: %s\n\n%s" % (ct, file(path).read())
+        req.header([('Content-type', ct),
+                    ('Content-length', os.path.getsize(path))])
+        return file(path).read()
     except (TypeError, OSError):
         # illegal fname or unreadable file
         return ""
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -10,9 +10,8 @@ import os
 import os.path
 import mimetypes
 from mercurial.demandload import demandload
-demandload(globals(), "re zlib ConfigParser cStringIO sys tempfile")
+demandload(globals(), "re zlib ConfigParser mimetools 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,9 +650,12 @@ 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)
+            header_file = cStringIO.StringIO(''.join(self.t("header", **map)))
+            msg = mimetools.Message(header_file, 0)
+            req.header(msg.items())
+            yield header_file.read()
 
         def footer(**map):
             yield self.t("footer",
@@ -724,7 +726,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
@@ -830,7 +831,7 @@ class hgweb(object):
         static = self.repo.ui.config("web", "static",
                                      os.path.join(self.templatepath,
                                                   "static"))
-        req.write(staticfile(static, fname)
+        req.write(staticfile(static, fname, req)
                   or self.t("error", error="%r not found" % fname))
 
     def do_capabilities(self, req):
--- a/mercurial/hgweb/hgwebdir_mod.py
+++ b/mercurial/hgweb/hgwebdir_mod.py
@@ -8,10 +8,9 @@
 
 import os
 from mercurial.demandload import demandload
-demandload(globals(), "ConfigParser")
+demandload(globals(), "ConfigParser mimetools cStringIO")
 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,9 +46,12 @@ 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)
+            header_file = cStringIO.StringIO(''.join(tmpl("header", **map)))
+            msg = mimetools.Message(header_file, 0)
+            req.header(msg.items())
+            yield header_file.read()
 
         def footer(**map):
             yield tmpl("footer", motd=self.motd, **map)
@@ -133,7 +135,7 @@ class hgwebdir(object):
             if req.form.has_key('static'):
                 static = os.path.join(templater.templatepath(), "static")
                 fname = req.form['static'][0]
-                req.write(staticfile(static, fname)
+                req.write(staticfile(static, fname, req)
                           or tmpl("error", error="%r not found" % fname))
             else:
                 sortable = ["name", "description", "contact", "lastchange"]
--- a/mercurial/hgweb/request.py
+++ b/mercurial/hgweb/request.py
@@ -10,13 +10,48 @@ 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)
@@ -27,23 +62,22 @@ class hgrequest(object):
                 for part in thing:
                     self.write(part)
             else:
+                thing = str(thing)
+                if self.server_write is None:
+                    if not self.headers:
+                        raise RuntimeError("request.write called before headers sent (%s)." % thing)
+                    self.server_write = self.start_response('200 Script output follows',
+                                                            self.headers)
+                    self.start_response = None
+                    self.headers = None
                 try:
-                    self.out.write(str(thing))
+                    self.server_write(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 +85,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()
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -225,6 +225,10 @@ def isodate(date):
     '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.'''
     return util.datestr(date, format='%Y-%m-%d %H:%M')
 
+def hgdate(date):
+    '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.'''
+    return "%d %d" % date
+
 def nl2br(text):
     '''replace raw newlines with xhtml line breaks.'''
     return text.replace('\n', '<br/>\n')
@@ -282,6 +286,7 @@ common_filters = {
     "fill76": lambda x: fill(x, width=76),
     "firstline": lambda x: x.splitlines(1)[0].rstrip('\r\n'),
     "tabindent": lambda x: indent(x, '\t'),
+    "hgdate": hgdate,
     "isodate": isodate,
     "obfuscate": obfuscate,
     "permissions": lambda x: x and "-rwxr-xr-x" or "-rw-r--r--",
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -859,6 +859,49 @@ def datestr(date=None, format='%a %b %d 
         s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
     return s
 
+def strdate(string, format='%a %b %d %H:%M:%S %Y'):
+    """parse a localized time string and return a (unixtime, offset) tuple.
+    if the string cannot be parsed, ValueError is raised."""
+    def hastimezone(string):
+        return (string[-4:].isdigit() and
+               (string[-5] == '+' or string[-5] == '-') and
+               string[-6].isspace())
+
+    if hastimezone(string):
+        date, tz = string.rsplit(None, 1)
+        tz = int(tz)
+        offset = - 3600 * (tz / 100) - 60 * (tz % 100)
+    else:
+        date, offset = string, 0
+    when = int(time.mktime(time.strptime(date, format))) + offset
+    return when, offset
+
+def parsedate(string, formats=('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M')):
+    """parse a localized time string and return a (unixtime, offset) tuple.
+    The date may be a "unixtime offset" string or in one of the specified
+    formats."""
+    try:
+        when, offset = map(int, string.split(' '))
+    except ValueError:
+        for format in formats:
+            try:
+                when, offset = strdate(string, format)
+            except ValueError:
+                pass
+            else:
+                break
+        else:
+            raise ValueError(_('invalid date: %r') % string)
+    # validate explicit (probably user-specified) date and
+    # time zone offset. values must fit in signed 32 bits for
+    # current 32-bit linux runtimes. timezones go from UTC-12
+    # to UTC+14
+    if abs(when) > 0x7fffffff:
+        raise ValueError(_('date exceeds 32 bits: %d') % when)
+    if offset < -50400 or offset > 43200:
+        raise ValueError(_('impossible time zone offset: %d') % offset)
+    return when, offset
+
 def shortuser(user):
     """Return a short representation of a user name or email address."""
     f = user.find('@')
--- a/templates/changeset-raw.tmpl
+++ b/templates/changeset-raw.tmpl
@@ -1,7 +1,7 @@
 #header#
 # HG changeset patch
 # User #author#
-# Date #date|date#
+# Date #date|hgdate#
 # Node ID #node#
 #parent%changesetparent#
 #desc#
--- a/templates/map-raw
+++ b/templates/map-raw
@@ -5,8 +5,8 @@ difflineplus = '#line#'
 difflineminus = '#line#'
 difflineat = '#line#'
 diffline = '#line#'
-changesetparent = '# parent: #node#'
-changesetchild = '# child: #node#'
+changesetparent = '# Parent #node#'
+changesetchild = '# Child #node#'
 filenodelink = ''
 filerevision = 'Content-Type: #mimetype#\nContent-Disposition: filename=#file#\n\n#raw#'
 fileline = '#line#'
new file mode 100755
--- /dev/null
+++ b/tests/test-import
@@ -0,0 +1,81 @@
+#!/bin/sh
+
+hg init a
+echo line 1 > a/a
+hg --cwd a ci -d '0 0' -Ama
+
+echo line 2 >> a/a
+hg --cwd a ci -u someone -d '1 0' -m'second change'
+
+echo % import exported patch
+hg clone -r0 a b
+hg --cwd a export tip > tip.patch
+hg --cwd b import ../tip.patch
+echo % message should be same
+hg --cwd b tip | grep 'second change'
+echo % committer should be same
+hg --cwd b tip | grep someone
+rm -rf b
+
+echo % import of plain diff should fail without message
+hg clone -r0 a b
+hg --cwd a diff -r0:1 > tip.patch
+hg --cwd b import ../tip.patch
+rm -rf b
+
+echo % import of plain diff should be ok with message
+hg clone -r0 a b
+hg --cwd a diff -r0:1 > tip.patch
+hg --cwd b import -mpatch ../tip.patch
+rm -rf b
+
+echo % import from stdin
+hg clone -r0 a b
+hg --cwd a export tip | hg --cwd b import -
+rm -rf b
+
+echo % override commit message
+hg clone -r0 a b
+hg --cwd a export tip | hg --cwd b import -m 'override' -
+hg --cwd b tip | grep override
+rm -rf b
+
+cat > mkmsg.py <<EOF
+import email.Message, sys
+msg = email.Message.Message()
+msg.set_payload('email commit message\n' + open('tip.patch').read())
+msg['Subject'] = 'email patch'
+msg['From'] = 'email patcher'
+sys.stdout.write(msg.as_string())
+EOF
+
+echo % plain diff in email, subject, message body
+hg clone -r0 a b
+hg --cwd a diff -r0:1 > tip.patch
+python mkmsg.py > msg.patch
+hg --cwd b import ../msg.patch
+hg --cwd b tip | grep email
+rm -rf b
+
+echo % plain diff in email, no subject, message body
+hg clone -r0 a b
+grep -v '^Subject:' msg.patch | hg --cwd b import -
+rm -rf b
+
+echo % plain diff in email, subject, no message body
+hg clone -r0 a b
+grep -v '^email ' msg.patch | hg --cwd b import -
+rm -rf b
+
+echo % plain diff in email, no subject, no message body, should fail
+hg clone -r0 a b
+grep -v '^\(Subject\|email\)' msg.patch | hg --cwd b import -
+rm -rf b
+
+echo % hg export in email, should use patch header
+hg clone -r0 a b
+hg --cwd a export tip > tip.patch
+python mkmsg.py | hg --cwd b import -
+hg --cwd b tip | grep second
+rm -rf b
+
new file mode 100644
--- /dev/null
+++ b/tests/test-import.out
@@ -0,0 +1,103 @@
+adding a
+% import exported patch
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying ../tip.patch
+patching file a
+% message should be same
+summary:     second change
+% committer should be same
+user:        someone
+% import of plain diff should fail without message
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying ../tip.patch
+patching file a
+transaction abort!
+rollback completed
+% import of plain diff should be ok with message
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying ../tip.patch
+patching file a
+% import from stdin
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying patch from stdin
+patching file a
+% override commit message
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying patch from stdin
+patching file a
+summary:     override
+% plain diff in email, subject, message body
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying ../msg.patch
+patching file a
+user:        email patcher
+summary:     email patch
+% plain diff in email, no subject, message body
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying patch from stdin
+patching file a
+% plain diff in email, subject, no message body
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying patch from stdin
+patching file a
+% plain diff in email, no subject, no message body, should fail
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying patch from stdin
+patching file a
+transaction abort!
+rollback completed
+% hg export in email, should use patch header
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+applying patch from stdin
+patching file a
+summary:     second change
--- a/tests/test-incoming-outgoing
+++ b/tests/test-incoming-outgoing
@@ -14,8 +14,10 @@ cd ..
 hg init new
 # http incoming
 http_proxy= hg -R new incoming http://localhost:20059/
+http_proxy= hg -R new incoming -r 4 http://localhost:20059/
 # local incoming
 hg -R new incoming test
+hg -R new incoming -r 4 test
 
 # test with --bundle
 http_proxy= hg -R new incoming --bundle test.hg http://localhost:20059/
@@ -42,5 +44,6 @@ hg verify
 cd ..
 hg -R test-dev outgoing test
 http_proxy= hg -R test-dev outgoing http://localhost:20059/
+http_proxy= hg -R test-dev outgoing -r 11 http://localhost:20059/
 
 kill `cat test/hg.pid`
--- a/tests/test-incoming-outgoing.out
+++ b/tests/test-incoming-outgoing.out
@@ -75,6 +75,31 @@ user:        test
 date:        Mon Jan 12 13:46:40 1970 +0000
 summary:     4
 
+changeset:   0:9cb21d99fe27
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     0
+
+changeset:   1:d717f5dfad6a
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     1
+
+changeset:   2:c0d6b86da426
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     2
+
+changeset:   3:dfacbd43b3fe
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     3
+
+changeset:   4:1f3a964b6022
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     4
+
 changeset:   5:c028bcc7a28a
 user:        test
 date:        Mon Jan 12 13:46:40 1970 +0000
@@ -121,6 +146,31 @@ user:        test
 date:        Mon Jan 12 13:46:40 1970 +0000
 summary:     4
 
+changeset:   0:9cb21d99fe27
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     0
+
+changeset:   1:d717f5dfad6a
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     1
+
+changeset:   2:c0d6b86da426
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     2
+
+changeset:   3:dfacbd43b3fe
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     3
+
+changeset:   4:1f3a964b6022
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     4
+
 changeset:   5:c028bcc7a28a
 user:        test
 date:        Mon Jan 12 13:46:40 1970 +0000
@@ -270,3 +320,19 @@ user:        test
 date:        Mon Jan 12 13:46:40 1970 +0000
 summary:     13
 
+searching for changes
+changeset:   9:3741c3ad1096
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     9
+
+changeset:   10:de4143c8d9a5
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     10
+
+changeset:   11:0e1c188b9a7a
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     11
+
new file mode 100755
--- /dev/null
+++ b/tests/test-parse-date
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+hg init
+echo "test-parse-date" > a
+hg add a
+hg ci -d "2006-02-01 13:00:30" -m "rev 0"
+echo "hi!" >> a
+hg ci -d "2006-02-01 13:00:30 -0500" -m "rev 1"
+hg tag -d "2006-04-15 13:30" "Hi"
+hg backout --merge -d "2006-04-15 13:30 +0200" -m "rev 3" 1
+hg ci -d "1150000000 14400" -m "rev 4 (merge)"
+echo "fail" >> a
+hg ci -d "should fail" -m "fail"
+hg ci -d "100000000000000000 1400" -m "fail"
+hg ci -d "100000 1400000" -m "fail"
+hg log --template '{date|date}\n'
new file mode 100644
--- /dev/null
+++ b/tests/test-parse-date.out
@@ -0,0 +1,19 @@
+reverting a
+changeset 3:107ce1ee2b43 backs out changeset 1:25a1420a55f8
+merging with changeset 2:99a1acecff55
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+abort: invalid date: 'should fail'
+transaction abort!
+rollback completed
+abort: date exceeds 32 bits: 100000000000000000
+transaction abort!
+rollback completed
+abort: impossible time zone offset: 1400000
+transaction abort!
+rollback completed
+Sun Jun 11 00:26:40 2006 -0400
+Sat Apr 15 13:30:00 2006 +0200
+Sat Apr 15 13:30:00 2006 +0000
+Wed Feb 01 13:00:30 2006 -0500
+Wed Feb 01 13:00:30 2006 +0000