changeset 5612:7c976bb039af

Merge with crew-stable
author Patrick Mezard <pmezard@gmail.com>
date Tue, 04 Dec 2007 23:28:10 +0100
parents 74f65f44a9aa (diff) 60bd4e707a7d (current diff)
children 2e76e5a23c2b
files mercurial/util_win32.py
diffstat 77 files changed, 3585 insertions(+), 1367 deletions(-) [+]
line wrap: on
line diff
--- a/CONTRIBUTORS	Tue Dec 04 23:10:13 2007 +0100
+++ b/CONTRIBUTORS	Tue Dec 04 23:28:10 2007 +0100
@@ -1,4 +1,7 @@
-Andrea Arcangeli <andrea at suse.de>
+[This file is here for historical purposes, all recent contributors
+should appear in the changelog directly]
+
+Andrea Arcangeli <andrea at suse.de>
 Thomas Arendsen Hein <thomas at intevation.de>
 Goffredo Baroncelli <kreijack at libero.it>
 Muli Ben-Yehuda <mulix at mulix.org>
@@ -36,5 +39,3 @@
 Rafael Villar Burke <pachi at mmn-arquitectos.com>
 Tristan Wibberley <tristan at wibberley.org>
 Mark Williamson <mark.williamson at cl.cam.ac.uk>
-
-If you are a contributor and don't see your name here, please let me know.
--- a/contrib/bash_completion	Tue Dec 04 23:10:13 2007 +0100
+++ b/contrib/bash_completion	Tue Dec 04 23:28:10 2007 +0100
@@ -305,6 +305,15 @@
     _hg_ext_mq_patchlist qunapplied
 }
 
+_hg_cmd_qgoto()
+{
+    if [[ "$prev" = @(-n|--name) ]]; then
+	_hg_ext_mq_queues
+	return
+    fi
+    _hg_ext_mq_patchlist qseries
+}
+
 _hg_cmd_qdelete()
 {
     local qcmd=qunapplied
--- a/contrib/hgk	Tue Dec 04 23:10:13 2007 +0100
+++ b/contrib/hgk	Tue Dec 04 23:28:10 2007 +0100
@@ -649,7 +649,7 @@
     if {$stuffsaved} return
     if {![winfo viewable .]} return
     catch {
-	set f [open "~/.gitk-new" w]
+	set f [open "~/.hgk-new" w]
 	puts $f [list set mainfont $mainfont]
 	puts $f [list set curidfont $curidfont]
 	puts $f [list set textfont $textfont]
@@ -687,7 +687,7 @@
 	puts $f "#"
 	puts $f "set authorcolors {$authorcolors}"
 	close $f
-	file rename -force "~/.gitk-new" "~/.gitk"
+	file rename -force "~/.hgk-new" "~/.hgk"
     }
     set stuffsaved 1
 }
@@ -3847,7 +3847,7 @@
     deeppink mediumorchid blue burlywood4 goldenrod slateblue red2 navy dimgrey
 }
 
-catch {source ~/.gitk}
+catch {source ~/.hgk}
 
 if {$curidfont == ""} {  # initialize late based on current mainfont
     set curidfont "$mainfont bold italic underline"
--- a/doc/hgrc.5.txt	Tue Dec 04 23:10:13 2007 +0100
+++ b/doc/hgrc.5.txt	Tue Dec 04 23:28:10 2007 +0100
@@ -17,7 +17,9 @@
 
 Mercurial reads configuration data from several files, if they exist.
 The names of these files depend on the system on which Mercurial is
-installed.
+installed. Windows registry keys contain PATH-like strings, every
+part must reference a Mercurial.ini file or be a directory where *.rc
+files will be read.
 
 (Unix)    <install-root>/etc/mercurial/hgrc.d/*.rc::
 (Unix)    <install-root>/etc/mercurial/hgrc::
@@ -29,6 +31,8 @@
 
 (Unix)    /etc/mercurial/hgrc.d/*.rc::
 (Unix)    /etc/mercurial/hgrc::
+(Windows) HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial::
+  or::
 (Windows) C:\Mercurial\Mercurial.ini::
     Per-system configuration files, for the system on which Mercurial
     is running.  Options in these files apply to all Mercurial
--- a/hg	Tue Dec 04 23:10:13 2007 +0100
+++ b/hg	Tue Dec 04 23:28:10 2007 +0100
@@ -10,5 +10,11 @@
 # enable importing on demand to reduce startup time
 from mercurial import demandimport; demandimport.enable()
 
+import sys
+import mercurial.util
 import mercurial.dispatch
+
+for fp in (sys.stdin, sys.stdout, sys.stderr):
+    mercurial.util.set_binary(fp)
+
 mercurial.dispatch.run()
--- a/hgext/convert/__init__.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/__init__.py	Tue Dec 04 23:28:10 2007 +0100
@@ -5,12 +5,12 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
-from common import NoRepo, SKIPREV, converter_source, converter_sink
+from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile
 from cvs import convert_cvs
 from darcs import darcs_source
 from git import convert_git
 from hg import mercurial_source, mercurial_sink
-from subversion import svn_source, debugsvnlog
+from subversion import debugsvnlog, svn_source, svn_sink
 import filemap
 
 import os, shutil
@@ -29,6 +29,7 @@
 
 sink_converters = [
     ('hg', mercurial_sink),
+    ('svn', svn_sink),
     ]
 
 def convertsource(ui, path, type, rev):
@@ -61,23 +62,10 @@
         self.ui = ui
         self.opts = opts
         self.commitcache = {}
-        self.revmapfile = revmapfile
-        self.revmapfilefd = None
         self.authors = {}
         self.authorfile = None
 
-        self.maporder = []
-        self.map = {}
-        try:
-            origrevmapfile = open(self.revmapfile, 'r')
-            for l in origrevmapfile:
-                sv, dv = l[:-1].split()
-                if sv not in self.map:
-                    self.maporder.append(sv)
-                self.map[sv] = dv
-            origrevmapfile.close()
-        except IOError:
-            pass
+        self.map = mapfile(ui, revmapfile)
 
         # Read first the dst author map if any
         authorfile = self.dest.authorfile()
@@ -165,16 +153,6 @@
 
         return s
 
-    def mapentry(self, src, dst):
-        if self.revmapfilefd is None:
-            try:
-                self.revmapfilefd = open(self.revmapfile, "a")
-            except IOError, (errno, strerror):
-                raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
-        self.map[src] = dst
-        self.revmapfilefd.write("%s %s\n" % (src, dst))
-        self.revmapfilefd.flush()
-
     def writeauthormap(self):
         authorfile = self.authorfile
         if authorfile:
@@ -221,7 +199,7 @@
                 dest = SKIPREV
             else:
                 dest = self.map[changes]
-            self.mapentry(rev, dest)
+            self.map[rev] = dest
             return
         files, copies = changes
         parents = [self.map[r] for r in commit.parents]
@@ -249,13 +227,14 @@
                         self.dest.copyfile(copyf, f)
 
         newnode = self.dest.putcommit(filenames, parents, commit)
-        self.mapentry(rev, newnode)
+        self.source.converted(rev, newnode)
+        self.map[rev] = newnode
 
     def convert(self):
         try:
             self.source.before()
             self.dest.before()
-            self.source.setrevmap(self.map, self.maporder)
+            self.source.setrevmap(self.map)
             self.ui.status("scanning source...\n")
             heads = self.source.getheads()
             parents = self.walktree(heads)
@@ -285,7 +264,7 @@
                 # write another hash correspondence to override the previous
                 # one so we don't end up with extra tag heads
                 if nrev:
-                    self.mapentry(c, nrev)
+                    self.map[c] = nrev
 
             self.writeauthormap()
         finally:
@@ -296,8 +275,7 @@
             self.dest.after()
         finally:
             self.source.after()
-        if self.revmapfilefd:
-            self.revmapfilefd.close()
+        self.map.close()
 
 def convert(ui, src, dest=None, revmapfile=None, **opts):
     """Convert a foreign SCM repository to a Mercurial one.
@@ -311,6 +289,7 @@
 
     Accepted destination formats:
     - Mercurial
+    - Subversion (history on branches is not preserved)
 
     If no revision is given, all revisions will be converted. Otherwise,
     convert will only import up to the named revision (given in a format
@@ -353,6 +332,24 @@
     The 'rename' directive renames a file or directory.  To rename from a
     subdirectory into the root of the repository, use '.' as the path to
     rename to.
+
+    Back end options:
+
+    --config convert.hg.clonebranches=False   (boolean)
+        hg target: XXX not documented
+    --config convert.hg.saverev=True          (boolean)
+        hg source: allow target to preserve source revision ID
+    --config convert.hg.tagsbranch=default    (branch name)
+        hg target: XXX not documented
+    --config convert.hg.usebranchnames=True   (boolean)
+        hg target: preserve branch names
+
+    --config convert.svn.branches=branches    (directory name)
+        svn source: specify the directory containing branches
+    --config convert.svn.tags=tags            (directory name)
+        svn source: specify the directory containing tags
+    --config convert.svn.trunk=trunk          (directory name)
+        svn source: specify the name of the trunk branch
     """
 
     util._encoding = 'UTF-8'
--- a/hgext/convert/common.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/common.py	Tue Dec 04 23:28:10 2007 +0100
@@ -1,6 +1,8 @@
 # common code for the convert extension
-import base64
+import base64, errno
 import cPickle as pickle
+from mercurial import util
+from mercurial.i18n import _
 
 def encodeargs(args):
     def encodearg(s):
@@ -15,6 +17,11 @@
     s = base64.decodestring(s)
     return pickle.loads(s)
 
+def checktool(exe, name=None):
+    name = name or exe
+    if not util.find_exe(exe):
+        raise util.Abort('cannot find required "%s" tool' % name)
+
 class NoRepo(Exception): pass
 
 SKIPREV = 'SKIP'
@@ -33,7 +40,7 @@
 class converter_source(object):
     """Conversion source interface"""
 
-    def __init__(self, ui, path, rev=None):
+    def __init__(self, ui, path=None, rev=None):
         """Initialize conversion source (or raise NoRepo("message")
         exception if path is not a valid repository)"""
         self.ui = ui
@@ -48,11 +55,8 @@
     def after(self):
         pass
 
-    def setrevmap(self, revmap, order):
-        """set the map of already-converted revisions
-        
-        order is a list with the keys from revmap in the order they
-        appear in the revision map file."""
+    def setrevmap(self, revmap):
+        """set the map of already-converted revisions"""
         pass
 
     def getheads(self):
@@ -111,6 +115,11 @@
         """
         raise NotImplementedError()
 
+    def converted(self, rev, sinkrev):
+        '''Notify the source that a revision has been converted.'''
+        pass
+
+
 class converter_sink(object):
     """Conversion sink (target) interface"""
 
@@ -184,3 +193,105 @@
         filter empty revisions.
         """
         pass
+
+    def before(self):
+        pass
+
+    def after(self):
+        pass
+
+
+class commandline(object):
+    def __init__(self, ui, command):
+        self.ui = ui
+        self.command = command
+
+    def prerun(self):
+        pass
+
+    def postrun(self):
+        pass
+
+    def _run(self, cmd, *args, **kwargs):
+        cmdline = [self.command, cmd] + list(args)
+        for k, v in kwargs.iteritems():
+            if len(k) == 1:
+                cmdline.append('-' + k)
+            else:
+                cmdline.append('--' + k.replace('_', '-'))
+            try:
+                if len(k) == 1:
+                    cmdline.append('' + v)
+                else:
+                    cmdline[-1] += '=' + v
+            except TypeError:
+                pass
+        cmdline = [util.shellquote(arg) for arg in cmdline]
+        cmdline += ['<', util.nulldev]
+        cmdline = ' '.join(cmdline)
+        self.ui.debug(cmdline, '\n')
+
+        self.prerun()
+        try:
+            return util.popen(cmdline)
+        finally:
+            self.postrun()
+
+    def run(self, cmd, *args, **kwargs):
+        fp = self._run(cmd, *args, **kwargs)
+        output = fp.read()
+        self.ui.debug(output)
+        return output, fp.close()
+
+    def checkexit(self, status, output=''):
+        if status:
+            if output:
+                self.ui.warn(_('%s error:\n') % self.command)
+                self.ui.warn(output)
+            msg = util.explain_exit(status)[0]
+            raise util.Abort(_('%s %s') % (self.command, msg))
+
+    def run0(self, cmd, *args, **kwargs):
+        output, status = self.run(cmd, *args, **kwargs)
+        self.checkexit(status, output)
+        return output
+
+
+class mapfile(dict):
+    def __init__(self, ui, path):
+        super(mapfile, self).__init__()
+        self.ui = ui
+        self.path = path
+        self.fp = None
+        self.order = []
+        self._read()
+
+    def _read(self):
+        try:
+            fp = open(self.path, 'r')
+        except IOError, err:
+            if err.errno != errno.ENOENT:
+                raise
+            return
+        for line in fp:
+            key, value = line[:-1].split(' ', 1)
+            if key not in self:
+                self.order.append(key)
+            super(mapfile, self).__setitem__(key, value)
+        fp.close()
+            
+    def __setitem__(self, key, value):
+        if self.fp is None:
+            try:
+                self.fp = open(self.path, 'a')
+            except IOError, err:
+                raise util.Abort(_('could not open map file %r: %s') %
+                                 (self.path, err.strerror))
+        self.fp.write('%s %s\n' % (key, value))
+        self.fp.flush()
+        super(mapfile, self).__setitem__(key, value)
+
+    def close(self):
+        if self.fp:
+            self.fp.close()
+            self.fp = None
--- a/hgext/convert/cvs.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/cvs.py	Tue Dec 04 23:28:10 2007 +0100
@@ -1,9 +1,10 @@
 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
 
 import os, locale, re, socket
+from cStringIO import StringIO
 from mercurial import util
 
-from common import NoRepo, commit, converter_source
+from common import NoRepo, commit, converter_source, checktool
 
 class convert_cvs(converter_source):
     def __init__(self, ui, path, rev=None):
@@ -13,6 +14,9 @@
         if not os.path.exists(cvs):
             raise NoRepo("%s does not look like a CVS checkout" % path)
 
+        for tool in ('cvsps', 'cvs'):
+            checktool(tool)
+
         self.changeset = {}
         self.files = {}
         self.tags = {}
@@ -206,6 +210,20 @@
         return self.heads
 
     def _getfile(self, name, rev):
+
+        def chunkedread(fp, count):
+            # file-objects returned by socked.makefile() do not handle
+            # large read() requests very well.
+            chunksize = 65536
+            output = StringIO()
+            while count > 0:
+                data = fp.read(min(count, chunksize))
+                if not data:
+                    raise util.Abort("%d bytes missing from remote file" % count)
+                count -= len(data)
+                output.write(data)
+            return output.getvalue()
+
         if rev.endswith("(DEAD)"):
             raise IOError
 
@@ -224,14 +242,14 @@
                 self.readp.readline() # entries
                 mode = self.readp.readline()[:-1]
                 count = int(self.readp.readline()[:-1])
-                data = self.readp.read(count)
+                data = chunkedread(self.readp, count)
             elif line.startswith(" "):
                 data += line[1:]
             elif line.startswith("M "):
                 pass
             elif line.startswith("Mbinary "):
                 count = int(self.readp.readline()[:-1])
-                data = self.readp.read(count)
+                data = chunkedread(self.readp, count)
             else:
                 if line == "ok\n":
                     return (data, "x" in mode and "x" or "")
--- a/hgext/convert/darcs.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/darcs.py	Tue Dec 04 23:28:10 2007 +0100
@@ -1,6 +1,6 @@
 # darcs support for the convert extension
 
-from common import NoRepo, commit, converter_source
+from common import NoRepo, checktool, commandline, commit, converter_source
 from mercurial.i18n import _
 from mercurial import util
 import os, shutil, tempfile
@@ -17,15 +17,18 @@
             except ImportError: ElementTree = None
 
 
-class darcs_source(converter_source):
+class darcs_source(converter_source, commandline):
     def __init__(self, ui, path, rev=None):
-        super(darcs_source, self).__init__(ui, path, rev=rev)
+        converter_source.__init__(self, ui, path, rev=rev)
+        commandline.__init__(self, ui, 'darcs')
 
         # check for _darcs, ElementTree, _darcs/inventory so that we can
         # easily skip test-convert-darcs if ElementTree is not around
         if not os.path.exists(os.path.join(path, '_darcs')):
             raise NoRepo("%s does not look like a darcs repo" % path)
 
+        checktool('darcs')
+
         if ElementTree is None:
             raise util.Abort(_("Python ElementTree module is not available"))
 
@@ -45,7 +48,8 @@
         output, status = self.run('init', repodir=self.tmppath)
         self.checkexit(status)
 
-        tree = self.xml('changes', '--xml-output', '--summary')
+        tree = self.xml('changes', xml_output=True, summary=True,
+                        repodir=self.path)
         tagname = None
         child = None
         for elt in tree.findall('patch'):
@@ -65,31 +69,9 @@
         self.ui.debug('cleaning up %s\n' % self.tmppath)
         shutil.rmtree(self.tmppath, ignore_errors=True)
 
-    def _run(self, cmd, *args, **kwargs):
-        cmdline = ['darcs', cmd, '--repodir', kwargs.get('repodir', self.path)]
-        cmdline += args
-        cmdline = [util.shellquote(arg) for arg in cmdline]
-        cmdline += ['<', util.nulldev]
-        cmdline = ' '.join(cmdline)
-        self.ui.debug(cmdline, '\n')
-        return util.popen(cmdline)
-
-    def run(self, cmd, *args, **kwargs):
-        fp = self._run(cmd, *args, **kwargs)
-        output = fp.read()
-        return output, fp.close()
-
-    def checkexit(self, status, output=''):
-        if status:
-            if output:
-                self.ui.warn(_('darcs error:\n'))
-                self.ui.warn(output)
-            msg = util.explain_exit(status)[0]
-            raise util.Abort(_('darcs %s') % msg)
-        
-    def xml(self, cmd, *opts):
+    def xml(self, cmd, **kwargs):
         etree = ElementTree()
-        fp = self._run(cmd, *opts)
+        fp = self._run(cmd, **kwargs)
         etree.parse(fp)
         self.checkexit(fp.close())
         return etree.getroot()
@@ -105,15 +87,15 @@
                       desc=desc.strip(), parents=self.parents[rev])
 
     def pull(self, rev):
-        output, status = self.run('pull', self.path, '--all',
-                                  '--match', 'hash %s' % rev,
-                                  '--no-test', '--no-posthook',
-                                  '--external-merge', '/bin/false',
+        output, status = self.run('pull', self.path, all=True,
+                                  match='hash %s' % rev,
+                                  no_test=True, no_posthook=True,
+                                  external_merge='/bin/false',
                                   repodir=self.tmppath)
         if status:
             if output.find('We have conflicts in') == -1:
                 self.checkexit(status, output)
-            output, status = self.run('revert', '--all', repodir=self.tmppath)
+            output, status = self.run('revert', all=True, repodir=self.tmppath)
             self.checkexit(status, output)
 
     def getchanges(self, rev):
--- a/hgext/convert/filemap.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/filemap.py	Tue Dec 04 23:28:10 2007 +0100
@@ -7,7 +7,7 @@
 import shlex
 from mercurial.i18n import _
 from mercurial import util
-from common import SKIPREV
+from common import SKIPREV, converter_source
 
 def rpairs(name):
     e = len(name)
@@ -110,9 +110,9 @@
 #   touch files we're interested in, but also merges that merge two
 #   or more interesting revisions.
 
-class filemap_source(object):
+class filemap_source(converter_source):
     def __init__(self, ui, baseconverter, filemap):
-        self.ui = ui
+        super(filemap_source, self).__init__(ui)
         self.base = baseconverter
         self.filemapper = filemapper(ui, filemap)
         self.commits = {}
@@ -128,7 +128,7 @@
         self.children = {}
         self.seenchildren = {}
 
-    def setrevmap(self, revmap, order):
+    def setrevmap(self, revmap):
         # rebuild our state to make things restartable
         #
         # To avoid calling getcommit for every revision that has already
@@ -143,7 +143,7 @@
         seen = {SKIPREV: SKIPREV}
         dummyset = util.set()
         converted = []
-        for rev in order:
+        for rev in revmap.order:
             mapped = revmap[rev]
             wanted = mapped not in seen
             if wanted:
@@ -157,7 +157,7 @@
                 arg = None
             converted.append((rev, wanted, arg))
         self.convertedorder = converted
-        return self.base.setrevmap(revmap, order)
+        return self.base.setrevmap(revmap)
 
     def rebuild(self):
         if self._rebuilt:
@@ -344,9 +344,3 @@
 
     def gettags(self):
         return self.base.gettags()
-
-    def before(self):
-        pass
-
-    def after(self):
-        pass
--- a/hgext/convert/git.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/git.py	Tue Dec 04 23:28:10 2007 +0100
@@ -3,7 +3,7 @@
 import os
 from mercurial import util
 
-from common import NoRepo, commit, converter_source
+from common import NoRepo, commit, converter_source, checktool
 
 class convert_git(converter_source):
     # Windows does not support GIT_DIR= construct while other systems
@@ -31,6 +31,9 @@
             path += "/.git"
         if not os.path.exists(path + "/objects"):
             raise NoRepo("%s does not look like a Git repo" % path)
+
+        checktool('git-rev-parse', 'git')
+
         self.path = path
 
     def getheads(self):
--- a/hgext/convert/hg.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/hg.py	Tue Dec 04 23:28:10 2007 +0100
@@ -1,10 +1,16 @@
 # hg backend for convert extension
 
-# Note for hg->hg conversion: Old versions of Mercurial didn't trim
-# the whitespace from the ends of commit messages, but new versions
-# do.  Changesets created by those older versions, then converted, may
-# thus have different hashes for changesets that are otherwise
-# identical.
+# Notes for hg->hg conversion:
+#
+# * Old versions of Mercurial didn't trim the whitespace from the ends
+#   of commit messages, but new versions do.  Changesets created by
+#   those older versions, then converted, may thus have different
+#   hashes for changesets that are otherwise identical.
+#
+# * By default, the source revision is stored in the converted
+#   revision.  This will cause the converted revision to have a
+#   different identity than the source.  To avoid this, use the
+#   following option: "--config convert.hg.saverev=false"
 
 
 import os, time
@@ -24,8 +30,6 @@
         if os.path.isdir(path) and len(os.listdir(path)) > 0:
             try:
                 self.repo = hg.repository(self.ui, path)
-                ui.status(_('destination %s is a Mercurial repository\n') %
-                          path)
             except hg.RepoError, err:
                 ui.print_exc()
                 raise NoRepo(err.args[0])
@@ -183,6 +187,7 @@
 class mercurial_source(converter_source):
     def __init__(self, ui, path, rev=None):
         converter_source.__init__(self, ui, path, rev)
+        self.saverev = ui.configbool('convert', 'hg.saverev', True)
         try:
             self.repo = hg.repository(self.ui, path)
             # try to provoke an exception if this isn't really a hg
@@ -195,6 +200,7 @@
         self.lastrev = None
         self.lastctx = None
         self._changescache = None
+        self.convertfp = None
 
     def changectx(self, rev):
         if self.lastrev != rev:
@@ -240,8 +246,12 @@
     def getcommit(self, rev):
         ctx = self.changectx(rev)
         parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
+        if self.saverev:
+            crev = rev
+        else:
+            crev = None
         return commit(author=ctx.user(), date=util.datestr(ctx.date()),
-                      desc=ctx.description(), parents=parents,
+                      desc=ctx.description(), rev=crev, parents=parents,
                       branch=ctx.branch(), extra=ctx.extra())
 
     def gettags(self):
@@ -258,3 +268,9 @@
 
         return changes[0] + changes[1] + changes[2]
 
+    def converted(self, rev, destrev):
+        if self.convertfp is None:
+            self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
+                                  'a')
+        self.convertfp.write('%s %s\n' % (destrev, rev))
+        self.convertfp.flush()
--- a/hgext/convert/subversion.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/convert/subversion.py	Tue Dec 04 23:28:10 2007 +0100
@@ -17,9 +17,13 @@
 
 import locale
 import os
+import re
 import sys
 import cPickle as pickle
-from mercurial import util
+import tempfile
+
+from mercurial import strutil, util
+from mercurial.i18n import _
 
 # Subversion stuff. Works best with very recent Python SVN bindings
 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
@@ -28,6 +32,7 @@
 from cStringIO import StringIO
 
 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
+from common import commandline, converter_sink, mapfile
 
 try:
     from svn.core import SubversionException, Pool
@@ -149,9 +154,15 @@
         self.head = self.revid(self.last_changed)
         self._changescache = None
 
-    def setrevmap(self, revmap, order):
+        if os.path.exists(os.path.join(url, '.svn/entries')):
+            self.wc = url
+        else:
+            self.wc = None
+        self.convertfp = None
+
+    def setrevmap(self, revmap):
         lastrevs = {}
-        for revid in revmap.keys():
+        for revid in revmap.iterkeys():
             uuid, module, revnum = self.revsplit(revid)
             lastrevnum = lastrevs.setdefault(module, revnum)
             if revnum > lastrevnum:
@@ -293,6 +304,15 @@
             self.ui.note('no tags found at revision %d\n' % start)
         return tags
 
+    def converted(self, rev, destrev):
+        if not self.wc:
+            return
+        if self.convertfp is None:
+            self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
+                                  'a')
+        self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
+        self.convertfp.flush()
+
     # -- helper functions --
 
     def revid(self, revnum, module=None):
@@ -664,3 +684,219 @@
         pool = Pool()
         rpath = '/'.join([self.base, path]).strip('/')
         return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
+
+pre_revprop_change = '''#!/bin/sh
+
+REPOS="$1"
+REV="$2"
+USER="$3"
+PROPNAME="$4"
+ACTION="$5"
+
+if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
+if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
+if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
+
+echo "Changing prohibited revision property" >&2
+exit 1
+'''
+
+class svn_sink(converter_sink, commandline):
+    commit_re = re.compile(r'Committed revision (\d+).', re.M)
+
+    def prerun(self):
+        if self.wc:
+            os.chdir(self.wc)
+
+    def postrun(self):
+        if self.wc:
+            os.chdir(self.cwd)
+
+    def join(self, name):
+        return os.path.join(self.wc, '.svn', name)
+        
+    def revmapfile(self):
+        return self.join('hg-shamap')
+
+    def authorfile(self):
+        return self.join('hg-authormap')
+
+    def __init__(self, ui, path):
+        converter_sink.__init__(self, ui, path)
+        commandline.__init__(self, ui, 'svn')
+        self.delete = []
+        self.wc = None
+        self.cwd = os.getcwd()
+
+        path = os.path.realpath(path)
+
+        created = False
+        if os.path.isfile(os.path.join(path, '.svn', 'entries')):
+            self.wc = path
+            self.run0('update')
+        else:
+            wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
+
+            if os.path.isdir(os.path.dirname(path)):
+                if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
+                    ui.status(_('initializing svn repo %r\n') %
+                              os.path.basename(path))
+                    commandline(ui, 'svnadmin').run0('create', path)
+                    created = path
+                path = path.replace('\\', '/')
+                if not path.startswith('/'):
+                    path = '/' + path
+                path = 'file://' + path
+            
+            ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
+            self.run0('checkout', path, wcpath)
+
+            self.wc = wcpath
+        self.opener = util.opener(self.wc)
+        self.wopener = util.opener(self.wc)
+        self.childmap = mapfile(ui, self.join('hg-childmap'))
+        self.is_exec = util.checkexec(self.wc) and util.is_exec or None
+
+        if created:
+            hook = os.path.join(created, 'hooks', 'pre-revprop-change')
+            fp = open(hook, 'w')
+            fp.write(pre_revprop_change)
+            fp.close()
+            util.set_exec(hook, True)
+
+        xport = transport.SvnRaTransport(url=geturl(path))
+        self.uuid = svn.ra.get_uuid(xport.ra)
+
+    def wjoin(self, *names):
+        return os.path.join(self.wc, *names)
+
+    def putfile(self, filename, flags, data):
+        if 'l' in flags:
+            self.wopener.symlink(data, filename)
+        else:
+            try:
+                if os.path.islink(self.wjoin(filename)):
+                    os.unlink(filename)
+            except OSError:
+                pass
+            self.wopener(filename, 'w').write(data)
+
+            if self.is_exec:
+                was_exec = self.is_exec(self.wjoin(filename))
+            else:
+                # On filesystems not supporting execute-bit, there is no way
+                # to know if it is set but asking subversion. Setting it
+                # systematically is just as expensive and much simpler.
+                was_exec = 'x' not in flags
+
+            util.set_exec(self.wjoin(filename), 'x' in flags)
+            if was_exec:
+                if 'x' not in flags:
+                    self.run0('propdel', 'svn:executable', filename)
+            else:
+                if 'x' in flags:
+                    self.run0('propset', 'svn:executable', '*', filename)
+            
+    def delfile(self, name):
+        self.delete.append(name)
+
+    def copyfile(self, source, dest):
+        # SVN's copy command pukes if the destination file exists, but
+        # our copyfile method expects to record a copy that has
+        # already occurred.  Cross the semantic gap.
+        wdest = self.wjoin(dest)
+        exists = os.path.exists(wdest)
+        if exists:
+            fd, tempname = tempfile.mkstemp(
+                prefix='hg-copy-', dir=os.path.dirname(wdest))
+            os.close(fd)
+            os.unlink(tempname)
+            os.rename(wdest, tempname)
+        try:
+            self.run0('copy', source, dest)
+        finally:
+            if exists:
+                try:
+                    os.unlink(wdest)
+                except OSError:
+                    pass
+                os.rename(tempname, wdest)
+
+    def dirs_of(self, files):
+        dirs = set()
+        for f in files:
+            if os.path.isdir(self.wjoin(f)):
+                dirs.add(f)
+            for i in strutil.rfindall(f, '/'):
+                dirs.add(f[:i])
+        return dirs
+
+    def add_files(self, files):
+        add_dirs = [d for d in self.dirs_of(files)
+                    if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
+        if add_dirs:
+            add_dirs.sort()
+            self.run('add', non_recursive=True, quiet=True, *add_dirs)
+        if files:
+            self.run('add', quiet=True, *files)
+        return files.union(add_dirs)
+        
+    def tidy_dirs(self, names):
+        dirs = list(self.dirs_of(names))
+        dirs.sort(reverse=True)
+        deleted = []
+        for d in dirs:
+            wd = self.wjoin(d)
+            if os.listdir(wd) == '.svn':
+                self.run0('delete', d)
+                deleted.append(d)
+        return deleted
+
+    def addchild(self, parent, child):
+        self.childmap[parent] = child
+
+    def revid(self, rev):
+        return u"svn:%s@%s" % (self.uuid, rev)
+        
+    def putcommit(self, files, parents, commit):
+        for parent in parents:
+            try:
+                return self.revid(self.childmap[parent])
+            except KeyError:
+                pass
+        entries = set(self.delete)
+        if self.delete:
+            self.run0('delete', *self.delete)
+            self.delete = []
+        files = util.frozenset(files)
+        entries.update(self.add_files(files.difference(entries)))
+        entries.update(self.tidy_dirs(entries))
+        fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
+        fp = os.fdopen(fd, 'w')
+        fp.write(commit.desc)
+        fp.close()
+        try:
+            output = self.run0('commit',
+                               username=util.shortuser(commit.author),
+                               file=messagefile,
+                               *list(entries))
+            try:
+                rev = self.commit_re.search(output).group(1)
+            except AttributeError:
+                self.ui.warn(_('unexpected svn output:\n'))
+                self.ui.warn(output)
+                raise util.Abort(_('unable to cope with svn output'))
+            if commit.rev:
+                self.run('propset', 'hg:convert-rev', commit.rev,
+                         revprop=True, revision=rev)
+            if commit.branch and commit.branch != 'default':
+                self.run('propset', 'hg:convert-branch', commit.branch,
+                         revprop=True, revision=rev)
+            for parent in parents:
+                self.addchild(parent, rev)
+            return self.revid(rev)
+        finally:
+            os.unlink(messagefile)
+
+    def puttags(self, tags):
+        self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
--- a/hgext/gpg.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgext/gpg.py	Tue Dec 04 23:28:10 2007 +0100
@@ -249,7 +249,7 @@
     message = opts['message']
     if not message:
         message = "\n".join([_("Added signature for changeset %s")
-                             % hgnode.hex(n)
+                             % hgnode.short(n)
                              for n in nodes])
     try:
         repo.commit([".hgsigs"], message, opts['user'], opts['date'])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/highlight.py	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,145 @@
+"""
+This is Mercurial extension for syntax highlighting in the file
+revision view of hgweb.
+
+It depends on the pygments syntax highlighting library:
+http://pygments.org/
+
+To enable the extension add this to hgrc:
+
+[extensions]
+hgext.highlight =
+
+There is a single configuration option:
+
+[web]
+pygments_style = <style>
+
+The default is 'colorful'.  If this is changed the corresponding CSS
+file should be re-generated by running
+
+# pygmentize -f html -S <newstyle>
+
+
+-- Adam Hupp <adam@hupp.org>
+
+
+"""
+
+from mercurial import demandimport
+demandimport.ignore.extend(['pkgutil',
+                            'pkg_resources',
+                            '__main__',])
+
+import mimetypes
+
+from mercurial.hgweb import hgweb_mod
+from mercurial.hgweb.hgweb_mod import hgweb
+from mercurial import util
+from mercurial.hgweb.common import paritygen
+from mercurial.node import hex
+
+from pygments import highlight
+from pygments.util import ClassNotFound
+from pygments.lexers import guess_lexer_for_filename, TextLexer
+from pygments.formatters import HtmlFormatter
+
+SYNTAX_CSS = ('\n<link rel="stylesheet" href="#staticurl#highlight.css" '
+              'type="text/css" />')
+
+class StripedHtmlFormatter(HtmlFormatter):
+    def __init__(self, stripecount, *args, **kwargs):
+        super(StripedHtmlFormatter, self).__init__(*args, **kwargs)
+        self.stripecount = stripecount
+
+    def wrap(self, source, outfile):
+        yield 0, "<div class='highlight'>"
+        yield 0, "<pre>"
+        parity = paritygen(self.stripecount)
+
+        for n, i in source:
+            if n == 1:
+                i = "<div class='parity%s'>%s</div>" % (parity.next(), i)
+            yield n, i
+
+        yield 0, "</pre>"
+        yield 0, "</div>"
+
+
+def pygments_format(filename, rawtext, forcetext=False, stripecount=1,
+                    style='colorful'):
+    if not forcetext:
+        try:
+            lexer = guess_lexer_for_filename(filename, rawtext)
+        except ClassNotFound:
+            lexer = TextLexer()
+    else:
+        lexer = TextLexer()
+
+    formatter = StripedHtmlFormatter(stripecount, style=style,
+                                     linenos='inline')
+
+    return highlight(rawtext, lexer, formatter)
+
+
+def filerevision_pygments(self, fctx):
+    """Reimplement hgweb.filerevision to use syntax highlighting"""
+    filename = fctx.path()
+
+    rawtext = fctx.data()
+    text = rawtext
+
+    mt = mimetypes.guess_type(filename)[0]
+
+    if util.binary(text):
+        mt = mt or 'application/octet-stream'
+        text = "(binary:%s)" % mt
+
+        # don't parse (binary:...) as anything
+        forcetext = True
+    else:
+        mt = mt or 'text/plain'
+        forcetext = False
+
+    def lines(text):
+        for line in text.splitlines(True):
+            yield {"line": line}
+
+    style = self.config("web", "pygments_style", "colorful")
+
+    text_formatted = lines(pygments_format(filename, text,
+                                           forcetext=forcetext,
+                                           stripecount=self.stripecount,
+                                           style=style))
+
+    # override per-line template
+    self.t.cache['fileline'] = '#line#'
+
+    # append a <link ...> to the syntax highlighting css
+    old_header = ''.join(self.t('header'))
+    if SYNTAX_CSS not in old_header:
+        new_header =  old_header + SYNTAX_CSS
+        self.t.cache['header'] = new_header
+
+    yield self.t("filerevision",
+                 file=filename,
+                 path=hgweb_mod._up(filename), # fixme: make public
+                 text=text_formatted,
+                 raw=rawtext,
+                 mimetype=mt,
+                 rev=fctx.rev(),
+                 node=hex(fctx.node()),
+                 author=fctx.user(),
+                 date=fctx.date(),
+                 desc=fctx.description(),
+                 parent=self.siblings(fctx.parents()),
+                 child=self.siblings(fctx.children()),
+                 rename=self.renamelink(fctx.filelog(),
+                                        fctx.filenode()),
+                 permissions=fctx.manifest().flags(filename))
+
+
+# monkeypatch in the new version
+# should be safer than overriding the method in a derived class
+# and then patching the class
+hgweb.filerevision = filerevision_pygments
--- a/hgweb.cgi	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgweb.cgi	Tue Dec 04 23:28:10 2007 +0100
@@ -22,10 +22,7 @@
 #os.environ["HGENCODING"] = "UTF-8"
 
 from mercurial.hgweb.hgweb_mod import hgweb
-from mercurial.hgweb.request import wsgiapplication
 import mercurial.hgweb.wsgicgi as wsgicgi
 
-def make_web_app():
-    return hgweb("/path/to/repo", "repository name")
-
-wsgicgi.launch(wsgiapplication(make_web_app))
+application = hgweb("/path/to/repo", "repository name")
+wsgicgi.launch(application)
--- a/hgwebdir.cgi	Tue Dec 04 23:10:13 2007 +0100
+++ b/hgwebdir.cgi	Tue Dec 04 23:28:10 2007 +0100
@@ -22,7 +22,6 @@
 #os.environ["HGENCODING"] = "UTF-8"
 
 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
@@ -44,7 +43,5 @@
 # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
 # or use a dictionary with entries like 'virtual/path': '/real/path'
 
-def make_web_app():
-    return hgwebdir("hgweb.config")
-
-wsgicgi.launch(wsgiapplication(make_web_app))
+application = hgwebdir('hgweb.config')
+wsgicgi.launch(application)
--- a/mercurial/bundlerepo.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/bundlerepo.py	Tue Dec 04 23:28:10 2007 +0100
@@ -48,7 +48,7 @@
                 continue
             for p in (p1, p2):
                 if not p in self.nodemap:
-                    raise revlog.LookupError(_("unknown parent %s") % short(p1))
+                    raise revlog.LookupError(hex(p1), _("unknown parent %s") % short(p1))
             if linkmapper is None:
                 link = n
             else:
--- a/mercurial/cmdutil.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/cmdutil.py	Tue Dec 04 23:28:10 2007 +0100
@@ -571,26 +571,26 @@
         def showcopies(**args):
             c = [{'name': x[0], 'source': x[1]} for x in copies]
             return showlist('file_copy', c, plural='file_copies', **args)
-
-        if self.ui.debugflag:
-            files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
-            def showfiles(**args):
-                return showlist('file', files[0], **args)
-            def showadds(**args):
-                return showlist('file_add', files[1], **args)
-            def showdels(**args):
-                return showlist('file_del', files[2], **args)
-            def showmanifest(**args):
-                args = args.copy()
-                args.update(dict(rev=self.repo.manifest.rev(changes[0]),
-                                 node=hex(changes[0])))
-                return self.t('manifest', **args)
-        else:
-            def showfiles(**args):
-                return showlist('file', changes[3], **args)
-            showadds = ''
-            showdels = ''
-            showmanifest = ''
+        
+        files = []
+        def getfiles():
+            if not files: 
+                files[:] = self.repo.status(
+                    log.parents(changenode)[0], changenode)[:3]
+            return files
+        def showfiles(**args):
+            return showlist('file', changes[3], **args)
+        def showmods(**args):
+            return showlist('file_mod', getfiles()[0], **args)
+        def showadds(**args):
+            return showlist('file_add', getfiles()[1], **args)
+        def showdels(**args):
+            return showlist('file_del', getfiles()[2], **args)
+        def showmanifest(**args):
+            args = args.copy()
+            args.update(dict(rev=self.repo.manifest.rev(changes[0]),
+                             node=hex(changes[0])))
+            return self.t('manifest', **args)
 
         defprops = {
             'author': changes[1],
@@ -599,6 +599,7 @@
             'desc': changes[4].strip(),
             'file_adds': showadds,
             'file_dels': showdels,
+            'file_mods': showmods,
             'files': showfiles,
             'file_copies': showcopies,
             'manifest': showmanifest,
--- a/mercurial/commands.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/commands.py	Tue Dec 04 23:28:10 2007 +0100
@@ -1629,7 +1629,7 @@
                 if opts.get('exact'):
                     if hex(n) != nodeid:
                         repo.rollback()
-                        raise util.Abort(_('patch is damaged' +
+                        raise util.Abort(_('patch is damaged'
                                            ' or loses information'))
             finally:
                 os.unlink(tmpname)
@@ -1937,7 +1937,7 @@
         if len(heads) == 1:
             msg = _('there is nothing to merge')
             if parent != repo.lookup(repo.workingctx().branch()):
-                msg = _('%s - use "hg update" instead' % msg)
+                msg = _('%s - use "hg update" instead') % msg
             raise util.Abort(msg)
 
         if parent not in heads:
--- a/mercurial/context.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/context.py	Tue Dec 04 23:28:10 2007 +0100
@@ -100,13 +100,13 @@
             try:
                 return self._manifest[path], self._manifest.flags(path)
             except KeyError:
-                raise revlog.LookupError(_("'%s' not found in manifest") % path)
+                raise revlog.LookupError(path, _("'%s' not found in manifest") % path)
         if '_manifestdelta' in self.__dict__ or path in self.files():
             if path in self._manifestdelta:
                 return self._manifestdelta[path], self._manifestdelta.flags(path)
         node, flag = self._repo.manifest.find(self._changeset[0], path)
         if not node:
-            raise revlog.LookupError(_("'%s' not found in manifest") % path)
+            raise revlog.LookupError(path, _("'%s' not found in manifest") % path)
 
         return node, flag
 
--- a/mercurial/dispatch.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/dispatch.py	Tue Dec 04 23:28:10 2007 +0100
@@ -125,7 +125,7 @@
             ui.warn("\n%r\n" % util.ellipsis(inst[1]))
     except ImportError, inst:
         m = str(inst).split()[-1]
-        ui.warn(_("abort: could not import module %s!\n" % m))
+        ui.warn(_("abort: could not import module %s!\n") % m)
         if m in "mpatch bdiff".split():
             ui.warn(_("(did you forget to compile extensions?)\n"))
         elif m in "zlib".split():
--- a/mercurial/hgweb/common.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/hgweb/common.py	Tue Dec 04 23:28:10 2007 +0100
@@ -6,7 +6,24 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
-import os, mimetypes
+import errno, mimetypes, os
+
+class ErrorResponse(Exception):
+    def __init__(self, code, message=None):
+        Exception.__init__(self)
+        self.code = code
+        if message:
+            self.message = message
+        else:
+            self.message = _statusmessage(code)
+
+def _statusmessage(code):
+    from BaseHTTPServer import BaseHTTPRequestHandler
+    responses = BaseHTTPRequestHandler.responses
+    return responses.get(code, ('Error', 'Unknown error'))[0]
+    
+def statusmessage(code):
+    return '%d %s' % (code, _statusmessage(code))
 
 def get_mtime(repo_path):
     store_path = os.path.join(repo_path, ".hg")
@@ -40,9 +57,13 @@
         req.header([('Content-type', ct),
                     ('Content-length', str(os.path.getsize(path)))])
         return file(path, 'rb').read()
-    except (TypeError, OSError):
-        # illegal fname or unreadable file
-        return ""
+    except TypeError:
+        raise ErrorResponse(500, 'illegal file name')
+    except OSError, err:
+        if err.errno == errno.ENOENT:
+            raise ErrorResponse(404)
+        else:
+            raise ErrorResponse(500, err.strerror)
 
 def style_map(templatepath, style):
     """Return path to mapfile for a given style.
--- a/mercurial/hgweb/hgweb_mod.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/hgweb/hgweb_mod.py	Tue Dec 04 23:28:10 2007 +0100
@@ -6,13 +6,28 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
-import os, mimetypes, re, zlib, mimetools, cStringIO, sys
-import tempfile, urllib, bz2
+import os, mimetypes, re, mimetools, cStringIO
 from mercurial.node import *
-from mercurial.i18n import gettext as _
-from mercurial import mdiff, ui, hg, util, archival, streamclone, patch
+from mercurial import mdiff, ui, hg, util, archival, patch
 from mercurial import revlog, templater
-from common import get_mtime, staticfile, style_map, paritygen
+from common import ErrorResponse, get_mtime, style_map, paritygen
+from request import wsgirequest
+import webcommands, protocol
+
+shortcuts = {
+    'cl': [('cmd', ['changelog']), ('rev', None)],
+    'sl': [('cmd', ['shortlog']), ('rev', None)],
+    'cs': [('cmd', ['changeset']), ('node', None)],
+    'f': [('cmd', ['file']), ('filenode', None)],
+    'fl': [('cmd', ['filelog']), ('filenode', None)],
+    'fd': [('cmd', ['filediff']), ('node', None)],
+    'fa': [('cmd', ['annotate']), ('filenode', None)],
+    'mf': [('cmd', ['manifest']), ('manifest', None)],
+    'ca': [('cmd', ['archive']), ('node', None)],
+    'tags': [('cmd', ['tags'])],
+    'tip': [('cmd', ['changeset']), ('node', ['tip'])],
+    'static': [('cmd', ['static']), ('file', None)]
+}
 
 def _up(p):
     if p[0] != "/":
@@ -106,17 +121,200 @@
             self.allowpull = self.configbool("web", "allowpull", True)
             self.encoding = self.config("web", "encoding", util._encoding)
 
+    def run(self):
+        if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
+            raise RuntimeError("This function is only intended to be called while running as a CGI script.")
+        import mercurial.hgweb.wsgicgi as wsgicgi
+        wsgicgi.launch(self)
+
+    def __call__(self, env, respond):
+        req = wsgirequest(env, respond)
+        self.run_wsgi(req)
+        return req
+
+    def run_wsgi(self, req):
+
+        self.refresh()
+
+        # expand form shortcuts
+
+        for k in shortcuts.iterkeys():
+            if k in req.form:
+                for name, value in shortcuts[k]:
+                    if value is None:
+                        value = req.form[k]
+                    req.form[name] = value
+                del req.form[k]
+
+        # work with CGI variables to create coherent structure
+        # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
+
+        req.url = req.env['SCRIPT_NAME']
+        if not req.url.endswith('/'):
+            req.url += '/'
+        if req.env.has_key('REPO_NAME'):
+            req.url += req.env['REPO_NAME'] + '/'
+
+        if req.env.get('PATH_INFO'):
+            parts = req.env.get('PATH_INFO').strip('/').split('/')
+            repo_parts = req.env.get('REPO_NAME', '').split('/')
+            if parts[:len(repo_parts)] == repo_parts:
+                parts = parts[len(repo_parts):]
+            query = '/'.join(parts)
+        else:
+            query = req.env['QUERY_STRING'].split('&', 1)[0]
+            query = query.split(';', 1)[0]
+
+        # translate user-visible url structure to internal structure
+
+        args = query.split('/', 2)
+        if 'cmd' not in req.form and args and args[0]:
+
+            cmd = args.pop(0)
+            style = cmd.rfind('-')
+            if style != -1:
+                req.form['style'] = [cmd[:style]]
+                cmd = cmd[style+1:]
+
+            # avoid accepting e.g. style parameter as command
+            if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
+                req.form['cmd'] = [cmd]
+
+            if args and args[0]:
+                node = args.pop(0)
+                req.form['node'] = [node]
+            if args:
+                req.form['file'] = args
+
+            if cmd == 'static':
+                req.form['file'] = req.form['node']
+            elif cmd == 'archive':
+                fn = req.form['node'][0]
+                for type_, spec in self.archive_specs.iteritems():
+                    ext = spec[2]
+                    if fn.endswith(ext):
+                        req.form['node'] = [fn[:-len(ext)]]
+                        req.form['type'] = [type_]
+
+        # actually process the request
+
+        try:
+
+            cmd = req.form.get('cmd', [''])[0]
+            if hasattr(protocol, cmd):
+                method = getattr(protocol, cmd)
+                method(self, req)
+            else:
+                tmpl = self.templater(req)
+                if cmd == '':
+                    req.form['cmd'] = [tmpl.cache['default']]
+                    cmd = req.form['cmd'][0]
+                method = getattr(webcommands, cmd)
+                method(self, req, tmpl)
+                del tmpl
+
+        except revlog.LookupError, err:
+            req.respond(404, tmpl(
+                        'error', error='revision not found: %s' % err.name))
+        except (hg.RepoError, revlog.RevlogError), inst:
+            req.respond('500 Internal Server Error',
+                        tmpl('error', error=str(inst)))
+        except ErrorResponse, inst:
+            req.respond(inst.code, tmpl('error', error=inst.message))
+        except AttributeError:
+            req.respond(400, tmpl('error', error='No such method: ' + cmd))
+
+    def templater(self, req):
+
+        # determine scheme, port and server name
+        # this is needed to create absolute urls
+
+        proto = req.env.get('wsgi.url_scheme')
+        if proto == 'https':
+            proto = 'https'
+            default_port = "443"
+        else:
+            proto = 'http'
+            default_port = "80"
+
+        port = req.env["SERVER_PORT"]
+        port = port != default_port and (":" + port) or ""
+        urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
+        staticurl = self.config("web", "staticurl") or req.url + 'static/'
+        if not staticurl.endswith('/'):
+            staticurl += '/'
+
+        # some functions for the templater
+
+        def header(**map):
+            header_file = cStringIO.StringIO(
+                ''.join(tmpl("header", encoding=self.encoding, **map)))
+            msg = mimetools.Message(header_file, 0)
+            req.header(msg.items())
+            yield header_file.read()
+
+        def rawfileheader(**map):
+            req.header([('Content-type', map['mimetype']),
+                        ('Content-disposition', 'filename=%s' % map['file']),
+                        ('Content-length', str(len(map['raw'])))])
+            yield ''
+
+        def footer(**map):
+            yield tmpl("footer", **map)
+
+        def motd(**map):
+            yield self.config("web", "motd", "")
+
+        def sessionvars(**map):
+            fields = []
+            if req.form.has_key('style'):
+                style = req.form['style'][0]
+                if style != self.config('web', 'style', ''):
+                    fields.append(('style', style))
+
+            separator = req.url[-1] == '?' and ';' or '?'
+            for name, value in fields:
+                yield dict(name=name, value=value, separator=separator)
+                separator = ';'
+
+        # figure out which style to use
+
+        style = self.config("web", "style", "")
+        if req.form.has_key('style'):
+            style = req.form['style'][0]
+        mapfile = style_map(self.templatepath, style)
+
+        if not self.reponame:
+            self.reponame = (self.config("web", "name")
+                             or req.env.get('REPO_NAME')
+                             or req.url.strip('/') or self.repo.root)
+
+        # create the templater
+
+        tmpl = templater.templater(mapfile, templater.common_filters,
+                                   defaults={"url": req.url,
+                                             "staticurl": staticurl,
+                                             "urlbase": urlbase,
+                                             "repo": self.reponame,
+                                             "header": header,
+                                             "footer": footer,
+                                             "motd": motd,
+                                             "rawfileheader": rawfileheader,
+                                             "sessionvars": sessionvars
+                                            })
+        return tmpl
+
     def archivelist(self, nodeid):
         allowed = self.configlist("web", "allow_archive")
         for i, spec in self.archive_specs.iteritems():
             if i in allowed or self.configbool("web", "allow" + i):
                 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
 
-    def listfilediffs(self, files, changeset):
+    def listfilediffs(self, tmpl, files, changeset):
         for f in files[:self.maxfiles]:
-            yield self.t("filedifflink", node=hex(changeset), file=f)
+            yield tmpl("filedifflink", node=hex(changeset), file=f)
         if len(files) > self.maxfiles:
-            yield self.t("fileellipses")
+            yield tmpl("fileellipses")
 
     def siblings(self, siblings=[], hiderev=None, **args):
         siblings = [s for s in siblings if s.node() != nullid]
@@ -148,11 +346,11 @@
             branches.append({"name": branch})
         return branches
 
-    def showtag(self, t1, node=nullid, **args):
+    def showtag(self, tmpl, t1, node=nullid, **args):
         for t in self.repo.nodetags(node):
-            yield self.t(t1, tag=t, **args)
+            yield tmpl(t1, tag=t, **args)
 
-    def diff(self, node1, node2, files):
+    def diff(self, tmpl, node1, node2, files):
         def filterfiles(filters, files):
             l = [x for x in files if x in filters]
 
@@ -164,22 +362,22 @@
 
         parity = paritygen(self.stripecount)
         def diffblock(diff, f, fn):
-            yield self.t("diffblock",
-                         lines=prettyprintlines(diff),
-                         parity=parity.next(),
-                         file=f,
-                         filenode=hex(fn or nullid))
+            yield tmpl("diffblock",
+                       lines=prettyprintlines(diff),
+                       parity=parity.next(),
+                       file=f,
+                       filenode=hex(fn or nullid))
 
         def prettyprintlines(diff):
             for l in diff.splitlines(1):
                 if l.startswith('+'):
-                    yield self.t("difflineplus", line=l)
+                    yield tmpl("difflineplus", line=l)
                 elif l.startswith('-'):
-                    yield self.t("difflineminus", line=l)
+                    yield tmpl("difflineminus", line=l)
                 elif l.startswith('@'):
-                    yield self.t("difflineat", line=l)
+                    yield tmpl("difflineat", line=l)
                 else:
-                    yield self.t("diffline", line=l)
+                    yield tmpl("diffline", line=l)
 
         r = self.repo
         c1 = r.changectx(node1)
@@ -209,7 +407,7 @@
             yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
                                           opts=diffopts), f, tn)
 
-    def changelog(self, ctx, shortlog=False):
+    def changelog(self, tmpl, ctx, shortlog=False):
         def changelist(limit=0,**map):
             cl = self.repo.changelog
             l = [] # build a list in forward order for efficiency
@@ -224,7 +422,7 @@
                              "changelogtag": self.showtag("changelogtag",n),
                              "desc": ctx.description(),
                              "date": ctx.date(),
-                             "files": self.listfilediffs(ctx.files(), n),
+                             "files": self.listfilediffs(tmpl, ctx.files(), n),
                              "rev": i,
                              "node": hex(n),
                              "tags": self.nodetagsdict(n),
@@ -247,15 +445,15 @@
 
         changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
 
-        yield self.t(shortlog and 'shortlog' or 'changelog',
-                     changenav=changenav,
-                     node=hex(cl.tip()),
-                     rev=pos, changesets=count,
-                     entries=lambda **x: changelist(limit=0,**x),
-                     latestentry=lambda **x: changelist(limit=1,**x),
-                     archives=self.archivelist("tip"))
+        yield tmpl(shortlog and 'shortlog' or 'changelog',
+                   changenav=changenav,
+                   node=hex(cl.tip()),
+                   rev=pos, changesets=count,
+                   entries=lambda **x: changelist(limit=0,**x),
+                   latestentry=lambda **x: changelist(limit=1,**x),
+                   archives=self.archivelist("tip"))
 
-    def search(self, query):
+    def search(self, tmpl, query):
 
         def changelist(**map):
             cl = self.repo.changelog
@@ -286,19 +484,19 @@
                 count += 1
                 n = ctx.node()
 
-                yield self.t('searchentry',
-                             parity=parity.next(),
-                             author=ctx.user(),
-                             parent=self.siblings(ctx.parents()),
-                             child=self.siblings(ctx.children()),
-                             changelogtag=self.showtag("changelogtag",n),
-                             desc=ctx.description(),
-                             date=ctx.date(),
-                             files=self.listfilediffs(ctx.files(), n),
-                             rev=ctx.rev(),
-                             node=hex(n),
-                             tags=self.nodetagsdict(n),
-                             branches=self.nodebranchdict(ctx))
+                yield tmpl('searchentry',
+                           parity=parity.next(),
+                           author=ctx.user(),
+                           parent=self.siblings(ctx.parents()),
+                           child=self.siblings(ctx.children()),
+                           changelogtag=self.showtag("changelogtag",n),
+                           desc=ctx.description(),
+                           date=ctx.date(),
+                           files=self.listfilediffs(tmpl, ctx.files(), n),
+                           rev=ctx.rev(),
+                           node=hex(n),
+                           tags=self.nodetagsdict(n),
+                           branches=self.nodebranchdict(ctx))
 
                 if count >= self.maxchanges:
                     break
@@ -306,13 +504,13 @@
         cl = self.repo.changelog
         parity = paritygen(self.stripecount)
 
-        yield self.t('search',
-                     query=query,
-                     node=hex(cl.tip()),
-                     entries=changelist,
-                     archives=self.archivelist("tip"))
+        yield tmpl('search',
+                   query=query,
+                   node=hex(cl.tip()),
+                   entries=changelist,
+                   archives=self.archivelist("tip"))
 
-    def changeset(self, ctx):
+    def changeset(self, tmpl, ctx):
         n = ctx.node()
         parents = ctx.parents()
         p1 = parents[0].node()
@@ -320,29 +518,29 @@
         files = []
         parity = paritygen(self.stripecount)
         for f in ctx.files():
-            files.append(self.t("filenodelink",
-                                node=hex(n), file=f,
-                                parity=parity.next()))
+            files.append(tmpl("filenodelink",
+                              node=hex(n), file=f,
+                              parity=parity.next()))
 
         def diff(**map):
-            yield self.diff(p1, n, None)
+            yield self.diff(tmpl, p1, n, None)
 
-        yield self.t('changeset',
-                     diff=diff,
-                     rev=ctx.rev(),
-                     node=hex(n),
-                     parent=self.siblings(parents),
-                     child=self.siblings(ctx.children()),
-                     changesettag=self.showtag("changesettag",n),
-                     author=ctx.user(),
-                     desc=ctx.description(),
-                     date=ctx.date(),
-                     files=files,
-                     archives=self.archivelist(hex(n)),
-                     tags=self.nodetagsdict(n),
-                     branches=self.nodebranchdict(ctx))
+        yield tmpl('changeset',
+                   diff=diff,
+                   rev=ctx.rev(),
+                   node=hex(n),
+                   parent=self.siblings(parents),
+                   child=self.siblings(ctx.children()),
+                   changesettag=self.showtag("changesettag",n),
+                   author=ctx.user(),
+                   desc=ctx.description(),
+                   date=ctx.date(),
+                   files=files,
+                   archives=self.archivelist(hex(n)),
+                   tags=self.nodetagsdict(n),
+                   branches=self.nodebranchdict(ctx))
 
-    def filelog(self, fctx):
+    def filelog(self, tmpl, fctx):
         f = fctx.path()
         fl = fctx.filelog()
         count = fl.count()
@@ -379,11 +577,11 @@
 
         nodefunc = lambda x: fctx.filectx(fileid=x)
         nav = revnavgen(pos, pagelen, count, nodefunc)
-        yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav,
-                     entries=lambda **x: entries(limit=0, **x),
-                     latestentry=lambda **x: entries(limit=1, **x))
+        yield tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
+                   entries=lambda **x: entries(limit=0, **x),
+                   latestentry=lambda **x: entries(limit=1, **x))
 
-    def filerevision(self, fctx):
+    def filerevision(self, tmpl, fctx):
         f = fctx.path()
         text = fctx.data()
         fl = fctx.filelog()
@@ -403,23 +601,23 @@
                        "linenumber": "% 6d" % (l + 1),
                        "parity": parity.next()}
 
-        yield self.t("filerevision",
-                     file=f,
-                     path=_up(f),
-                     text=lines(),
-                     raw=rawtext,
-                     mimetype=mt,
-                     rev=fctx.rev(),
-                     node=hex(fctx.node()),
-                     author=fctx.user(),
-                     date=fctx.date(),
-                     desc=fctx.description(),
-                     parent=self.siblings(fctx.parents()),
-                     child=self.siblings(fctx.children()),
-                     rename=self.renamelink(fl, n),
-                     permissions=fctx.manifest().flags(f))
+        yield tmpl("filerevision",
+                   file=f,
+                   path=_up(f),
+                   text=lines(),
+                   raw=rawtext,
+                   mimetype=mt,
+                   rev=fctx.rev(),
+                   node=hex(fctx.node()),
+                   author=fctx.user(),
+                   date=fctx.date(),
+                   desc=fctx.description(),
+                   parent=self.siblings(fctx.parents()),
+                   child=self.siblings(fctx.children()),
+                   rename=self.renamelink(fl, n),
+                   permissions=fctx.manifest().flags(f))
 
-    def fileannotate(self, fctx):
+    def fileannotate(self, tmpl, fctx):
         f = fctx.path()
         n = fctx.filenode()
         fl = fctx.filelog()
@@ -441,21 +639,21 @@
                        "file": f.path(),
                        "line": l}
 
-        yield self.t("fileannotate",
-                     file=f,
-                     annotate=annotate,
-                     path=_up(f),
-                     rev=fctx.rev(),
-                     node=hex(fctx.node()),
-                     author=fctx.user(),
-                     date=fctx.date(),
-                     desc=fctx.description(),
-                     rename=self.renamelink(fl, n),
-                     parent=self.siblings(fctx.parents()),
-                     child=self.siblings(fctx.children()),
-                     permissions=fctx.manifest().flags(f))
+        yield tmpl("fileannotate",
+                   file=f,
+                   annotate=annotate,
+                   path=_up(f),
+                   rev=fctx.rev(),
+                   node=hex(fctx.node()),
+                   author=fctx.user(),
+                   date=fctx.date(),
+                   desc=fctx.description(),
+                   rename=self.renamelink(fl, n),
+                   parent=self.siblings(fctx.parents()),
+                   child=self.siblings(fctx.children()),
+                   permissions=fctx.manifest().flags(f))
 
-    def manifest(self, ctx, path):
+    def manifest(self, tmpl, ctx, path):
         mf = ctx.manifest()
         node = ctx.node()
 
@@ -478,6 +676,9 @@
                 short = os.path.basename(remain)
                 files[short] = (f, n)
 
+        if not files:
+            raise ErrorResponse(404, 'Path not found: ' + path)
+
         def filelist(**map):
             fl = files.keys()
             fl.sort()
@@ -506,19 +707,19 @@
                        "path": "%s%s" % (abspath, f),
                        "basename": f[:-1]}
 
-        yield self.t("manifest",
-                     rev=ctx.rev(),
-                     node=hex(node),
-                     path=abspath,
-                     up=_up(abspath),
-                     upparity=parity.next(),
-                     fentries=filelist,
-                     dentries=dirlist,
-                     archives=self.archivelist(hex(node)),
-                     tags=self.nodetagsdict(node),
-                     branches=self.nodebranchdict(ctx))
+        yield tmpl("manifest",
+                   rev=ctx.rev(),
+                   node=hex(node),
+                   path=abspath,
+                   up=_up(abspath),
+                   upparity=parity.next(),
+                   fentries=filelist,
+                   dentries=dirlist,
+                   archives=self.archivelist(hex(node)),
+                   tags=self.nodetagsdict(node),
+                   branches=self.nodebranchdict(ctx))
 
-    def tags(self):
+    def tags(self, tmpl):
         i = self.repo.tagslist()
         i.reverse()
         parity = paritygen(self.stripecount)
@@ -536,13 +737,13 @@
                        "date": self.repo.changectx(n).date(),
                        "node": hex(n)}
 
-        yield self.t("tags",
-                     node=hex(self.repo.changelog.tip()),
-                     entries=lambda **x: entries(False,0, **x),
-                     entriesnotip=lambda **x: entries(True,0, **x),
-                     latestentry=lambda **x: entries(True,1, **x))
+        yield tmpl("tags",
+                   node=hex(self.repo.changelog.tip()),
+                   entries=lambda **x: entries(False,0, **x),
+                   entriesnotip=lambda **x: entries(True,0, **x),
+                   latestentry=lambda **x: entries(True,1, **x))
 
-    def summary(self):
+    def summary(self, tmpl):
         i = self.repo.tagslist()
         i.reverse()
 
@@ -557,11 +758,11 @@
                 if count > 10: # limit to 10 tags
                     break;
 
-                yield self.t("tagentry",
-                             parity=parity.next(),
-                             tag=k,
-                             node=hex(n),
-                             date=self.repo.changectx(n).date())
+                yield tmpl("tagentry",
+                           parity=parity.next(),
+                           tag=k,
+                           node=hex(n),
+                           date=self.repo.changectx(n).date())
 
 
         def branches(**map):
@@ -587,8 +788,8 @@
                 n = ctx.node()
                 hn = hex(n)
 
-                l.insert(0, self.t(
-                    'shortlogentry',
+                l.insert(0, tmpl(
+                   'shortlogentry',
                     parity=parity.next(),
                     author=ctx.user(),
                     desc=ctx.description(),
@@ -605,34 +806,34 @@
         start = max(0, count - self.maxchanges)
         end = min(count, start + self.maxchanges)
 
-        yield self.t("summary",
-                 desc=self.config("web", "description", "unknown"),
-                 owner=(self.config("ui", "username") or # preferred
-                        self.config("web", "contact") or # deprecated
-                        self.config("web", "author", "unknown")), # also
-                 lastchange=cl.read(cl.tip())[2],
-                 tags=tagentries,
-                 branches=branches,
-                 shortlog=changelist,
-                 node=hex(cl.tip()),
-                 archives=self.archivelist("tip"))
+        yield tmpl("summary",
+                   desc=self.config("web", "description", "unknown"),
+                   owner=(self.config("ui", "username") or # preferred
+                          self.config("web", "contact") or # deprecated
+                          self.config("web", "author", "unknown")), # also
+                   lastchange=cl.read(cl.tip())[2],
+                   tags=tagentries,
+                   branches=branches,
+                   shortlog=changelist,
+                   node=hex(cl.tip()),
+                   archives=self.archivelist("tip"))
 
-    def filediff(self, fctx):
+    def filediff(self, tmpl, fctx):
         n = fctx.node()
         path = fctx.path()
         parents = fctx.parents()
         p1 = parents and parents[0].node() or nullid
 
         def diff(**map):
-            yield self.diff(p1, n, [path])
+            yield self.diff(tmpl, p1, n, [path])
 
-        yield self.t("filediff",
-                     file=path,
-                     node=hex(n),
-                     rev=fctx.rev(),
-                     parent=self.siblings(parents),
-                     child=self.siblings(fctx.children()),
-                     diff=diff)
+        yield tmpl("filediff",
+                   file=path,
+                   node=hex(n),
+                   rev=fctx.rev(),
+                   parent=self.siblings(parents),
+                   child=self.siblings(fctx.children()),
+                   diff=diff)
 
     archive_specs = {
         'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
@@ -640,7 +841,7 @@
         'zip': ('application/zip', 'zip', '.zip', None),
         }
 
-    def archive(self, req, key, type_):
+    def archive(self, tmpl, req, key, type_):
         reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
         cnode = self.repo.lookup(key)
         arch_version = key
@@ -664,198 +865,6 @@
         path = path.lstrip('/')
         return util.canonpath(self.repo.root, '', path)
 
-    def run(self):
-        if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
-            raise RuntimeError("This function is only intended to be called while running as a CGI script.")
-        import mercurial.hgweb.wsgicgi as wsgicgi
-        from request import wsgiapplication
-        def make_web_app():
-            return self
-        wsgicgi.launch(wsgiapplication(make_web_app))
-
-    def run_wsgi(self, req):
-        def header(**map):
-            header_file = cStringIO.StringIO(
-                ''.join(self.t("header", encoding=self.encoding, **map)))
-            msg = mimetools.Message(header_file, 0)
-            req.header(msg.items())
-            yield header_file.read()
-
-        def rawfileheader(**map):
-            req.header([('Content-type', map['mimetype']),
-                        ('Content-disposition', 'filename=%s' % map['file']),
-                        ('Content-length', str(len(map['raw'])))])
-            yield ''
-
-        def footer(**map):
-            yield self.t("footer", **map)
-
-        def motd(**map):
-            yield self.config("web", "motd", "")
-
-        def expand_form(form):
-            shortcuts = {
-                'cl': [('cmd', ['changelog']), ('rev', None)],
-                'sl': [('cmd', ['shortlog']), ('rev', None)],
-                'cs': [('cmd', ['changeset']), ('node', None)],
-                'f': [('cmd', ['file']), ('filenode', None)],
-                'fl': [('cmd', ['filelog']), ('filenode', None)],
-                'fd': [('cmd', ['filediff']), ('node', None)],
-                'fa': [('cmd', ['annotate']), ('filenode', None)],
-                'mf': [('cmd', ['manifest']), ('manifest', None)],
-                'ca': [('cmd', ['archive']), ('node', None)],
-                'tags': [('cmd', ['tags'])],
-                'tip': [('cmd', ['changeset']), ('node', ['tip'])],
-                'static': [('cmd', ['static']), ('file', None)]
-            }
-
-            for k in shortcuts.iterkeys():
-                if form.has_key(k):
-                    for name, value in shortcuts[k]:
-                        if value is None:
-                            value = form[k]
-                        form[name] = value
-                    del form[k]
-
-        def rewrite_request(req):
-            '''translate new web interface to traditional format'''
-
-            def spliturl(req):
-                def firstitem(query):
-                    return query.split('&', 1)[0].split(';', 1)[0]
-
-                def normurl(url):
-                    inner = '/'.join([x for x in url.split('/') if x])
-                    tl = len(url) > 1 and url.endswith('/') and '/' or ''
-
-                    return '%s%s%s' % (url.startswith('/') and '/' or '',
-                                       inner, tl)
-
-                root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0]))
-                pi = normurl(req.env.get('PATH_INFO', ''))
-                if pi:
-                    # strip leading /
-                    pi = pi[1:]
-                    if pi:
-                        root = root[:root.rfind(pi)]
-                    if req.env.has_key('REPO_NAME'):
-                        rn = req.env['REPO_NAME'] + '/'
-                        root += rn
-                        query = pi[len(rn):]
-                    else:
-                        query = pi
-                else:
-                    root += '?'
-                    query = firstitem(req.env['QUERY_STRING'])
-
-                return (root, query)
-
-            req.url, query = spliturl(req)
-
-            if req.form.has_key('cmd'):
-                # old style
-                return
-
-            args = query.split('/', 2)
-            if not args or not args[0]:
-                return
-
-            cmd = args.pop(0)
-            style = cmd.rfind('-')
-            if style != -1:
-                req.form['style'] = [cmd[:style]]
-                cmd = cmd[style+1:]
-            # avoid accepting e.g. style parameter as command
-            if hasattr(self, 'do_' + cmd):
-                req.form['cmd'] = [cmd]
-
-            if args and args[0]:
-                node = args.pop(0)
-                req.form['node'] = [node]
-            if args:
-                req.form['file'] = args
-
-            if cmd == 'static':
-                req.form['file'] = req.form['node']
-            elif cmd == 'archive':
-                fn = req.form['node'][0]
-                for type_, spec in self.archive_specs.iteritems():
-                    ext = spec[2]
-                    if fn.endswith(ext):
-                        req.form['node'] = [fn[:-len(ext)]]
-                        req.form['type'] = [type_]
-
-        def sessionvars(**map):
-            fields = []
-            if req.form.has_key('style'):
-                style = req.form['style'][0]
-                if style != self.config('web', 'style', ''):
-                    fields.append(('style', style))
-
-            separator = req.url[-1] == '?' and ';' or '?'
-            for name, value in fields:
-                yield dict(name=name, value=value, separator=separator)
-                separator = ';'
-
-        self.refresh()
-
-        expand_form(req.form)
-        rewrite_request(req)
-
-        style = self.config("web", "style", "")
-        if req.form.has_key('style'):
-            style = req.form['style'][0]
-        mapfile = style_map(self.templatepath, style)
-
-        proto = req.env.get('wsgi.url_scheme')
-        if proto == 'https':
-            proto = 'https'
-            default_port = "443"
-        else:
-            proto = 'http'
-            default_port = "80"
-
-        port = req.env["SERVER_PORT"]
-        port = port != default_port and (":" + port) or ""
-        urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
-        staticurl = self.config("web", "staticurl") or req.url + 'static/'
-        if not staticurl.endswith('/'):
-            staticurl += '/'
-
-        if not self.reponame:
-            self.reponame = (self.config("web", "name")
-                             or req.env.get('REPO_NAME')
-                             or req.url.strip('/') or self.repo.root)
-
-        self.t = templater.templater(mapfile, templater.common_filters,
-                                     defaults={"url": req.url,
-                                               "staticurl": staticurl,
-                                               "urlbase": urlbase,
-                                               "repo": self.reponame,
-                                               "header": header,
-                                               "footer": footer,
-                                               "motd": motd,
-                                               "rawfileheader": rawfileheader,
-                                               "sessionvars": sessionvars
-                                               })
-
-        try:
-            if not req.form.has_key('cmd'):
-                req.form['cmd'] = [self.t.cache['default']]
-
-            cmd = req.form['cmd'][0]
-
-            method = getattr(self, 'do_' + cmd, None)
-            if method:
-                try:
-                    method(req)
-                except (hg.RepoError, revlog.RevlogError), inst:
-                    req.write(self.t("error", error=str(inst)))
-            else:
-                req.write(self.t("error", error='No such method: ' + cmd))
-        finally:
-            self.t = None
-
     def changectx(self, req):
         if req.form.has_key('node'):
             changeid = req.form['node'][0]
@@ -887,181 +896,6 @@
 
         return fctx
 
-    def do_log(self, req):
-        if req.form.has_key('file') and req.form['file'][0]:
-            self.do_filelog(req)
-        else:
-            self.do_changelog(req)
-
-    def do_rev(self, req):
-        self.do_changeset(req)
-
-    def do_file(self, req):
-        path = self.cleanpath(req.form.get('file', [''])[0])
-        if path:
-            try:
-                req.write(self.filerevision(self.filectx(req)))
-                return
-            except revlog.LookupError:
-                pass
-
-        req.write(self.manifest(self.changectx(req), path))
-
-    def do_diff(self, req):
-        self.do_filediff(req)
-
-    def do_changelog(self, req, shortlog = False):
-        if req.form.has_key('node'):
-            ctx = self.changectx(req)
-        else:
-            if req.form.has_key('rev'):
-                hi = req.form['rev'][0]
-            else:
-                hi = self.repo.changelog.count() - 1
-            try:
-                ctx = self.repo.changectx(hi)
-            except hg.RepoError:
-                req.write(self.search(hi)) # XXX redirect to 404 page?
-                return
-
-        req.write(self.changelog(ctx, shortlog = shortlog))
-
-    def do_shortlog(self, req):
-        self.do_changelog(req, shortlog = True)
-
-    def do_changeset(self, req):
-        req.write(self.changeset(self.changectx(req)))
-
-    def do_manifest(self, req):
-        req.write(self.manifest(self.changectx(req),
-                                self.cleanpath(req.form['path'][0])))
-
-    def do_tags(self, req):
-        req.write(self.tags())
-
-    def do_summary(self, req):
-        req.write(self.summary())
-
-    def do_filediff(self, req):
-        req.write(self.filediff(self.filectx(req)))
-
-    def do_annotate(self, req):
-        req.write(self.fileannotate(self.filectx(req)))
-
-    def do_filelog(self, req):
-        req.write(self.filelog(self.filectx(req)))
-
-    def do_lookup(self, req):
-        try:
-            r = hex(self.repo.lookup(req.form['key'][0]))
-            success = 1
-        except Exception,inst:
-            r = str(inst)
-            success = 0
-        resp = "%s %s\n" % (success, r)
-        req.httphdr("application/mercurial-0.1", length=len(resp))
-        req.write(resp)
-
-    def do_heads(self, req):
-        resp = " ".join(map(hex, self.repo.heads())) + "\n"
-        req.httphdr("application/mercurial-0.1", length=len(resp))
-        req.write(resp)
-
-    def do_branches(self, req):
-        nodes = []
-        if req.form.has_key('nodes'):
-            nodes = map(bin, req.form['nodes'][0].split(" "))
-        resp = cStringIO.StringIO()
-        for b in self.repo.branches(nodes):
-            resp.write(" ".join(map(hex, b)) + "\n")
-        resp = resp.getvalue()
-        req.httphdr("application/mercurial-0.1", length=len(resp))
-        req.write(resp)
-
-    def do_between(self, req):
-        if req.form.has_key('pairs'):
-            pairs = [map(bin, p.split("-"))
-                     for p in req.form['pairs'][0].split(" ")]
-        resp = cStringIO.StringIO()
-        for b in self.repo.between(pairs):
-            resp.write(" ".join(map(hex, b)) + "\n")
-        resp = resp.getvalue()
-        req.httphdr("application/mercurial-0.1", length=len(resp))
-        req.write(resp)
-
-    def do_changegroup(self, req):
-        req.httphdr("application/mercurial-0.1")
-        nodes = []
-        if not self.allowpull:
-            return
-
-        if req.form.has_key('roots'):
-            nodes = map(bin, req.form['roots'][0].split(" "))
-
-        z = zlib.compressobj()
-        f = self.repo.changegroup(nodes, 'serve')
-        while 1:
-            chunk = f.read(4096)
-            if not chunk:
-                break
-            req.write(z.compress(chunk))
-
-        req.write(z.flush())
-
-    def do_changegroupsubset(self, req):
-        req.httphdr("application/mercurial-0.1")
-        bases = []
-        heads = []
-        if not self.allowpull:
-            return
-
-        if req.form.has_key('bases'):
-            bases = [bin(x) for x in req.form['bases'][0].split(' ')]
-        if req.form.has_key('heads'):
-            heads = [bin(x) for x in req.form['heads'][0].split(' ')]
-
-        z = zlib.compressobj()
-        f = self.repo.changegroupsubset(bases, heads, 'serve')
-        while 1:
-            chunk = f.read(4096)
-            if not chunk:
-                break
-            req.write(z.compress(chunk))
-
-        req.write(z.flush())
-
-    def do_archive(self, req):
-        type_ = req.form['type'][0]
-        allowed = self.configlist("web", "allow_archive")
-        if (type_ in self.archives and (type_ in allowed or
-            self.configbool("web", "allow" + type_, False))):
-            self.archive(req, req.form['node'][0], type_)
-            return
-
-        req.write(self.t("error"))
-
-    def do_static(self, req):
-        fname = req.form['file'][0]
-        # a repo owner may set web.static in .hg/hgrc to get any file
-        # readable by the user running the CGI script
-        static = self.config("web", "static",
-                             os.path.join(self.templatepath, "static"),
-                             untrusted=False)
-        req.write(staticfile(static, fname, req)
-                  or self.t("error", error="%r not found" % fname))
-
-    def do_capabilities(self, req):
-        caps = ['lookup', 'changegroupsubset']
-        if self.configbool('server', 'uncompressed'):
-            caps.append('stream=%d' % self.repo.changelog.version)
-        # XXX: make configurable and/or share code with do_unbundle:
-        unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
-        if unbundleversions:
-            caps.append('unbundle=%s' % ','.join(unbundleversions))
-        resp = ' '.join(caps)
-        req.httphdr("application/mercurial-0.1", length=len(resp))
-        req.write(resp)
-
     def check_perm(self, req, op, default):
         '''check permission for operation based on user auth.
         return true if op allowed, else false.
@@ -1075,134 +909,3 @@
 
         allow = self.configlist('web', 'allow_' + op)
         return (allow and (allow == ['*'] or user in allow)) or default
-
-    def do_unbundle(self, req):
-        def bail(response, headers={}):
-            length = int(req.env['CONTENT_LENGTH'])
-            for s in util.filechunkiter(req, limit=length):
-                # drain incoming bundle, else client will not see
-                # response when run outside cgi script
-                pass
-            req.httphdr("application/mercurial-0.1", headers=headers)
-            req.write('0\n')
-            req.write(response)
-
-        # require ssl by default, auth info cannot be sniffed and
-        # replayed
-        ssl_req = self.configbool('web', 'push_ssl', True)
-        if ssl_req:
-            if req.env.get('wsgi.url_scheme') != 'https':
-                bail(_('ssl required\n'))
-                return
-            proto = 'https'
-        else:
-            proto = 'http'
-
-        # do not allow push unless explicitly allowed
-        if not self.check_perm(req, 'push', False):
-            bail(_('push not authorized\n'),
-                 headers={'status': '401 Unauthorized'})
-            return
-
-        their_heads = req.form['heads'][0].split(' ')
-
-        def check_heads():
-            heads = map(hex, self.repo.heads())
-            return their_heads == [hex('force')] or their_heads == heads
-
-        # fail early if possible
-        if not check_heads():
-            bail(_('unsynced changes\n'))
-            return
-
-        req.httphdr("application/mercurial-0.1")
-
-        # do not lock repo until all changegroup data is
-        # streamed. save to temporary file.
-
-        fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
-        fp = os.fdopen(fd, 'wb+')
-        try:
-            length = int(req.env['CONTENT_LENGTH'])
-            for s in util.filechunkiter(req, limit=length):
-                fp.write(s)
-
-            try:
-                lock = self.repo.lock()
-                try:
-                    if not check_heads():
-                        req.write('0\n')
-                        req.write(_('unsynced changes\n'))
-                        return
-
-                    fp.seek(0)
-                    header = fp.read(6)
-                    if not header.startswith("HG"):
-                        # old client with uncompressed bundle
-                        def generator(f):
-                            yield header
-                            for chunk in f:
-                                yield chunk
-                    elif not header.startswith("HG10"):
-                        req.write("0\n")
-                        req.write(_("unknown bundle version\n"))
-                        return
-                    elif header == "HG10GZ":
-                        def generator(f):
-                            zd = zlib.decompressobj()
-                            for chunk in f:
-                                yield zd.decompress(chunk)
-                    elif header == "HG10BZ":
-                        def generator(f):
-                            zd = bz2.BZ2Decompressor()
-                            zd.decompress("BZ")
-                            for chunk in f:
-                                yield zd.decompress(chunk)
-                    elif header == "HG10UN":
-                        def generator(f):
-                            for chunk in f:
-                                yield chunk
-                    else:
-                        req.write("0\n")
-                        req.write(_("unknown bundle compression type\n"))
-                        return
-                    gen = generator(util.filechunkiter(fp, 4096))
-
-                    # send addchangegroup output to client
-
-                    old_stdout = sys.stdout
-                    sys.stdout = cStringIO.StringIO()
-
-                    try:
-                        url = 'remote:%s:%s' % (proto,
-                                                req.env.get('REMOTE_HOST', ''))
-                        try:
-                            ret = self.repo.addchangegroup(
-                                        util.chunkbuffer(gen), 'serve', url)
-                        except util.Abort, inst:
-                            sys.stdout.write("abort: %s\n" % inst)
-                            ret = 0
-                    finally:
-                        val = sys.stdout.getvalue()
-                        sys.stdout = old_stdout
-                    req.write('%d\n' % ret)
-                    req.write(val)
-                finally:
-                    del lock
-            except (OSError, IOError), inst:
-                req.write('0\n')
-                filename = getattr(inst, 'filename', '')
-                # Don't send our filesystem layout to the client
-                if filename.startswith(self.repo.root):
-                    filename = filename[len(self.repo.root)+1:]
-                else:
-                    filename = ''
-                error = getattr(inst, 'strerror', 'Unknown error')
-                req.write('%s: %s\n' % (error, filename))
-        finally:
-            fp.close()
-            os.unlink(tempname)
-
-    def do_stream_out(self, req):
-        req.httphdr("application/mercurial-0.1")
-        streamclone.stream_out(self.repo, req, untrusted=True)
--- a/mercurial/hgweb/hgwebdir_mod.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/hgweb/hgwebdir_mod.py	Tue Dec 04 23:28:10 2007 +0100
@@ -9,8 +9,9 @@
 import os, mimetools, cStringIO
 from mercurial.i18n import gettext as _
 from mercurial import ui, hg, util, templater
-from common import get_mtime, staticfile, style_map, paritygen
+from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen
 from hgweb_mod import hgweb
+from request import wsgirequest
 
 # This is a stopgap
 class hgwebdir(object):
@@ -19,7 +20,8 @@
             return [(util.pconvert(name).strip('/'), path)
                     for name, path in items]
 
-        self.parentui = parentui
+        self.parentui = parentui or ui.ui(report_untrusted=False,
+                                          interactive = False)
         self.motd = None
         self.style = None
         self.stripecount = None
@@ -60,60 +62,74 @@
         if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
             raise RuntimeError("This function is only intended to be called while running as a CGI script.")
         import mercurial.hgweb.wsgicgi as wsgicgi
-        from request import wsgiapplication
-        def make_web_app():
-            return self
-        wsgicgi.launch(wsgiapplication(make_web_app))
+        wsgicgi.launch(self)
+
+    def __call__(self, env, respond):
+        req = wsgirequest(env, respond)
+        self.run_wsgi(req)
+        return req
 
     def run_wsgi(self, req):
-        def header(**map):
-            header_file = cStringIO.StringIO(
-                ''.join(tmpl("header", encoding=util._encoding, **map)))
-            msg = mimetools.Message(header_file, 0)
-            req.header(msg.items())
-            yield header_file.read()
 
-        def footer(**map):
-            yield tmpl("footer", **map)
+        try:
+            try:
 
-        def motd(**map):
-            if self.motd is not None:
-                yield self.motd
-            else:
-                yield config('web', 'motd', '')
+                virtual = req.env.get("PATH_INFO", "").strip('/')
+                
+                # a static file
+                if virtual.startswith('static/') or 'static' in req.form:
+                    static = os.path.join(templater.templatepath(), 'static')
+                    if virtual.startswith('static/'):
+                        fname = virtual[7:]
+                    else:
+                        fname = req.form['static'][0]
+                    req.write(staticfile(static, fname, req))
+                    return
 
-        parentui = self.parentui or ui.ui(report_untrusted=False,
-                                          interactive=False)
-
-        def config(section, name, default=None, untrusted=True):
-            return parentui.config(section, name, default, untrusted)
+                # top-level index
+                elif not virtual:
+                    tmpl = self.templater(req)
+                    self.makeindex(req, tmpl)
+                    return
 
-        url = req.env['REQUEST_URI'].split('?')[0]
-        if not url.endswith('/'):
-            url += '/'
-        pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/'
-        base = url[:len(url) - len(pathinfo)]
-        if not base.endswith('/'):
-            base += '/'
-
-        staticurl = config('web', 'staticurl') or base + 'static/'
-        if not staticurl.endswith('/'):
-            staticurl += '/'
+                # nested indexes and hgwebs
+                repos = dict(self.repos)
+                while virtual:
+                    real = repos.get(virtual)
+                    if real:
+                        req.env['REPO_NAME'] = virtual
+                        try:
+                            repo = hg.repository(self.parentui, real)
+                            hgweb(repo).run_wsgi(req)
+                            return
+                        except IOError, inst:
+                            raise ErrorResponse(500, inst.strerror)
+                        except hg.RepoError, inst:
+                            raise ErrorResponse(500, str(inst))
 
-        style = self.style
-        if style is None:
-            style = config('web', 'style', '')
-        if req.form.has_key('style'):
-            style = req.form['style'][0]
-        if self.stripecount is None:
-            self.stripecount = int(config('web', 'stripes', 1))
-        mapfile = style_map(templater.templatepath(), style)
-        tmpl = templater.templater(mapfile, templater.common_filters,
-                                   defaults={"header": header,
-                                             "footer": footer,
-                                             "motd": motd,
-                                             "url": url,
-                                             "staticurl": staticurl})
+                    # browse subdirectories
+                    subdir = virtual + '/'
+                    if [r for r in repos if r.startswith(subdir)]:
+                        tmpl = self.templater(req)
+                        self.makeindex(req, tmpl, subdir)
+                        return
+
+                    up = virtual.rfind('/')
+                    if up < 0:
+                        break
+                    virtual = virtual[:up]
+
+                # prefixes not found
+                tmpl = self.templater(req)
+                req.respond(404, tmpl("notfound", repo=virtual))
+                
+            except ErrorResponse, err:
+                tmpl = self.templater(req)
+                req.respond(err.code, tmpl('error', error=err.message or ''))
+        finally:
+            tmpl = None
+
+    def makeindex(self, req, tmpl, subdir=""):
 
         def archivelist(ui, nodeid, url):
             allowed = ui.configlist("web", "allow_archive", untrusted=True)
@@ -143,7 +159,7 @@
                     continue
                 name = name[len(subdir):]
 
-                u = ui.ui(parentui=parentui)
+                u = ui.ui(parentui=self.parentui)
                 try:
                     u.readconfig(os.path.join(path, '.hg', 'hgrc'))
                 except Exception, e:
@@ -155,8 +171,10 @@
                 if u.configbool("web", "hidden", untrusted=True):
                     continue
 
-                url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
-                       .replace("//", "/")) + '/'
+                parts = [req.env['PATH_INFO'], name]
+                if req.env['SCRIPT_NAME']:
+                	parts.insert(0, req.env['SCRIPT_NAME'])
+                url = ('/'.join(parts).replace("//", "/")) + '/'
 
                 # update time with local timezone
                 try:
@@ -195,66 +213,65 @@
                     row['parity'] = parity.next()
                     yield row
 
-        def makeindex(req, subdir=""):
-            sortable = ["name", "description", "contact", "lastchange"]
-            sortcolumn, descending = self.repos_sorted
-            if req.form.has_key('sort'):
-                sortcolumn = req.form['sort'][0]
-                descending = sortcolumn.startswith('-')
-                if descending:
-                    sortcolumn = sortcolumn[1:]
-                if sortcolumn not in sortable:
-                    sortcolumn = ""
+        sortable = ["name", "description", "contact", "lastchange"]
+        sortcolumn, descending = self.repos_sorted
+        if req.form.has_key('sort'):
+            sortcolumn = req.form['sort'][0]
+            descending = sortcolumn.startswith('-')
+            if descending:
+                sortcolumn = sortcolumn[1:]
+            if sortcolumn not in sortable:
+                sortcolumn = ""
 
-            sort = [("sort_%s" % column,
-                     "%s%s" % ((not descending and column == sortcolumn)
-                               and "-" or "", column))
-                    for column in sortable]
-            req.write(tmpl("index", entries=entries, subdir=subdir,
-                           sortcolumn=sortcolumn, descending=descending,
-                           **dict(sort)))
+        sort = [("sort_%s" % column,
+                 "%s%s" % ((not descending and column == sortcolumn)
+                            and "-" or "", column))
+                for column in sortable]
+        req.write(tmpl("index", entries=entries, subdir=subdir,
+                       sortcolumn=sortcolumn, descending=descending,
+                       **dict(sort)))
+
+    def templater(self, req):
+
+        def header(**map):
+            header_file = cStringIO.StringIO(
+                ''.join(tmpl("header", encoding=util._encoding, **map)))
+            msg = mimetools.Message(header_file, 0)
+            req.header(msg.items())
+            yield header_file.read()
+
+        def footer(**map):
+            yield tmpl("footer", **map)
 
-        try:
-            virtual = req.env.get("PATH_INFO", "").strip('/')
-            if virtual.startswith('static/'):
-                static = os.path.join(templater.templatepath(), 'static')
-                fname = virtual[7:]
-                req.write(staticfile(static, fname, req) or
-                          tmpl('error', error='%r not found' % fname))
-            elif virtual:
-                repos = dict(self.repos)
-                while virtual:
-                    real = repos.get(virtual)
-                    if real:
-                        req.env['REPO_NAME'] = virtual
-                        try:
-                            repo = hg.repository(parentui, real)
-                            hgweb(repo).run_wsgi(req)
-                        except IOError, inst:
-                            req.write(tmpl("error", error=inst.strerror))
-                        except hg.RepoError, inst:
-                            req.write(tmpl("error", error=str(inst)))
-                        return
+        def motd(**map):
+            if self.motd is not None:
+                yield self.motd
+            else:
+                yield config('web', 'motd', '')
+
+        def config(section, name, default=None, untrusted=True):
+            return self.parentui.config(section, name, default, untrusted)
+
+        url = req.env.get('SCRIPT_NAME', '')
+        if not url.endswith('/'):
+            url += '/'
 
-                    # browse subdirectories
-                    subdir = virtual + '/'
-                    if [r for r in repos if r.startswith(subdir)]:
-                        makeindex(req, subdir)
-                        return
-
-                    up = virtual.rfind('/')
-                    if up < 0:
-                        break
-                    virtual = virtual[:up]
+        staticurl = config('web', 'staticurl') or url + 'static/'
+        if not staticurl.endswith('/'):
+            staticurl += '/'
 
-                req.write(tmpl("notfound", repo=virtual))
-            else:
-                if req.form.has_key('static'):
-                    static = os.path.join(templater.templatepath(), "static")
-                    fname = req.form['static'][0]
-                    req.write(staticfile(static, fname, req)
-                              or tmpl("error", error="%r not found" % fname))
-                else:
-                    makeindex(req)
-        finally:
-            tmpl = None
+        style = self.style
+        if style is None:
+            style = config('web', 'style', '')
+        if req.form.has_key('style'):
+            style = req.form['style'][0]
+        if self.stripecount is None:
+            self.stripecount = int(config('web', 'stripes', 1))
+        mapfile = style_map(templater.templatepath(), style)
+        tmpl = templater.templater(mapfile, templater.common_filters,
+                                   defaults={"header": header,
+                                             "footer": footer,
+                                             "motd": motd,
+                                             "url": url,
+                                             "staticurl": staticurl})
+        return tmpl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/hgweb/protocol.py	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,237 @@
+#
+# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
+# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+import cStringIO, zlib, bz2, tempfile, errno, os, sys
+from mercurial import util, streamclone
+from mercurial.i18n import gettext as _
+from mercurial.node import *
+
+def lookup(web, req):
+    try:
+        r = hex(web.repo.lookup(req.form['key'][0]))
+        success = 1
+    except Exception,inst:
+        r = str(inst)
+        success = 0
+    resp = "%s %s\n" % (success, r)
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+def heads(web, req):
+    resp = " ".join(map(hex, web.repo.heads())) + "\n"
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+def branches(web, req):
+    nodes = []
+    if req.form.has_key('nodes'):
+        nodes = map(bin, req.form['nodes'][0].split(" "))
+    resp = cStringIO.StringIO()
+    for b in web.repo.branches(nodes):
+        resp.write(" ".join(map(hex, b)) + "\n")
+    resp = resp.getvalue()
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+def between(web, req):
+    if req.form.has_key('pairs'):
+        pairs = [map(bin, p.split("-"))
+                 for p in req.form['pairs'][0].split(" ")]
+    resp = cStringIO.StringIO()
+    for b in web.repo.between(pairs):
+        resp.write(" ".join(map(hex, b)) + "\n")
+    resp = resp.getvalue()
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+def changegroup(web, req):
+    req.httphdr("application/mercurial-0.1")
+    nodes = []
+    if not web.allowpull:
+        return
+
+    if req.form.has_key('roots'):
+        nodes = map(bin, req.form['roots'][0].split(" "))
+
+    z = zlib.compressobj()
+    f = web.repo.changegroup(nodes, 'serve')
+    while 1:
+        chunk = f.read(4096)
+        if not chunk:
+            break
+        req.write(z.compress(chunk))
+
+    req.write(z.flush())
+
+def changegroupsubset(web, req):
+    req.httphdr("application/mercurial-0.1")
+    bases = []
+    heads = []
+    if not web.allowpull:
+        return
+
+    if req.form.has_key('bases'):
+        bases = [bin(x) for x in req.form['bases'][0].split(' ')]
+    if req.form.has_key('heads'):
+        heads = [bin(x) for x in req.form['heads'][0].split(' ')]
+
+    z = zlib.compressobj()
+    f = web.repo.changegroupsubset(bases, heads, 'serve')
+    while 1:
+        chunk = f.read(4096)
+        if not chunk:
+            break
+        req.write(z.compress(chunk))
+
+    req.write(z.flush())
+
+def capabilities(web, req):
+    caps = ['lookup', 'changegroupsubset']
+    if web.configbool('server', 'uncompressed'):
+        caps.append('stream=%d' % web.repo.changelog.version)
+    # XXX: make configurable and/or share code with do_unbundle:
+    unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
+    if unbundleversions:
+        caps.append('unbundle=%s' % ','.join(unbundleversions))
+    resp = ' '.join(caps)
+    req.httphdr("application/mercurial-0.1", length=len(resp))
+    req.write(resp)
+
+def unbundle(web, req):
+    def bail(response, headers={}):
+        length = int(req.env['CONTENT_LENGTH'])
+        for s in util.filechunkiter(req, limit=length):
+            # drain incoming bundle, else client will not see
+            # response when run outside cgi script
+            pass
+        req.httphdr("application/mercurial-0.1", headers=headers)
+        req.write('0\n')
+        req.write(response)
+
+    # require ssl by default, auth info cannot be sniffed and
+    # replayed
+    ssl_req = web.configbool('web', 'push_ssl', True)
+    if ssl_req:
+        if req.env.get('wsgi.url_scheme') != 'https':
+            bail(_('ssl required\n'))
+            return
+        proto = 'https'
+    else:
+        proto = 'http'
+
+    # do not allow push unless explicitly allowed
+    if not web.check_perm(req, 'push', False):
+        bail(_('push not authorized\n'),
+             headers={'status': '401 Unauthorized'})
+        return
+
+    their_heads = req.form['heads'][0].split(' ')
+
+    def check_heads():
+        heads = map(hex, web.repo.heads())
+        return their_heads == [hex('force')] or their_heads == heads
+
+    # fail early if possible
+    if not check_heads():
+        bail(_('unsynced changes\n'))
+        return
+
+    req.httphdr("application/mercurial-0.1")
+
+    # do not lock repo until all changegroup data is
+    # streamed. save to temporary file.
+
+    fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
+    fp = os.fdopen(fd, 'wb+')
+    try:
+        length = int(req.env['CONTENT_LENGTH'])
+        for s in util.filechunkiter(req, limit=length):
+            fp.write(s)
+
+        try:
+            lock = web.repo.lock()
+            try:
+                if not check_heads():
+                    req.write('0\n')
+                    req.write(_('unsynced changes\n'))
+                    return
+
+                fp.seek(0)
+                header = fp.read(6)
+                if not header.startswith("HG"):
+                    # old client with uncompressed bundle
+                    def generator(f):
+                        yield header
+                        for chunk in f:
+                            yield chunk
+                elif not header.startswith("HG10"):
+                    req.write("0\n")
+                    req.write(_("unknown bundle version\n"))
+                    return
+                elif header == "HG10GZ":
+                    def generator(f):
+                        zd = zlib.decompressobj()
+                        for chunk in f:
+                            yield zd.decompress(chunk)
+                elif header == "HG10BZ":
+                    def generator(f):
+                        zd = bz2.BZ2Decompressor()
+                        zd.decompress("BZ")
+                        for chunk in f:
+                            yield zd.decompress(chunk)
+                elif header == "HG10UN":
+                    def generator(f):
+                        for chunk in f:
+                            yield chunk
+                else:
+                    req.write("0\n")
+                    req.write(_("unknown bundle compression type\n"))
+                    return
+                gen = generator(util.filechunkiter(fp, 4096))
+
+                # send addchangegroup output to client
+
+                old_stdout = sys.stdout
+                sys.stdout = cStringIO.StringIO()
+
+                try:
+                    url = 'remote:%s:%s' % (proto,
+                                            req.env.get('REMOTE_HOST', ''))
+                    try:
+                        ret = web.repo.addchangegroup(
+                                    util.chunkbuffer(gen), 'serve', url)
+                    except util.Abort, inst:
+                        sys.stdout.write("abort: %s\n" % inst)
+                        ret = 0
+                finally:
+                    val = sys.stdout.getvalue()
+                    sys.stdout = old_stdout
+                req.write('%d\n' % ret)
+                req.write(val)
+            finally:
+                del lock
+        except (OSError, IOError), inst:
+            req.write('0\n')
+            filename = getattr(inst, 'filename', '')
+            # Don't send our filesystem layout to the client
+            if filename.startswith(web.repo.root):
+                filename = filename[len(web.repo.root)+1:]
+            else:
+                filename = ''
+            error = getattr(inst, 'strerror', 'Unknown error')
+            if inst.errno == errno.ENOENT:
+                code = 404
+            else:
+                code = 500
+            req.respond(code, '%s: %s\n' % (error, filename))
+    finally:
+        fp.close()
+        os.unlink(tempname)
+
+def stream_out(web, req):
+    req.httphdr("application/mercurial-0.1")
+    streamclone.stream_out(web.repo, req, untrusted=True)
--- a/mercurial/hgweb/request.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/hgweb/request.py	Tue Dec 04 23:28:10 2007 +0100
@@ -8,16 +8,10 @@
 
 import socket, cgi, errno
 from mercurial.i18n import gettext as _
-
-class wsgiapplication(object):
-    def __init__(self, destmaker):
-        self.destmaker = destmaker
+from common import ErrorResponse, statusmessage
 
-    def __call__(self, wsgienv, start_response):
-        return _wsgirequest(self.destmaker(), wsgienv, start_response)
-
-class _wsgirequest(object):
-    def __init__(self, destination, wsgienv, start_response):
+class wsgirequest(object):
+    def __init__(self, wsgienv, start_response):
         version = wsgienv['wsgi.version']
         if (version < (1, 0)) or (version >= (2, 0)):
             raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
@@ -32,7 +26,6 @@
         self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
         self.start_response = start_response
         self.headers = []
-        destination.run_wsgi(self)
 
     out = property(lambda self: self)
 
@@ -42,25 +35,32 @@
     def read(self, count=-1):
         return self.inp.read(count)
 
-    def write(self, *things):
+    def respond(self, status, *things):
         for thing in things:
             if hasattr(thing, "__iter__"):
                 for part in thing:
-                    self.write(part)
+                    self.respond(status, 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',
+                    if isinstance(status, ErrorResponse):
+                        status = statusmessage(status.code)
+                    elif isinstance(status, int):
+                        status = statusmessage(status)
+                    self.server_write = self.start_response(status,
                                                             self.headers)
                     self.start_response = None
-                    self.headers = None
+                    self.headers = []
                 try:
                     self.server_write(thing)
                 except socket.error, inst:
                     if inst[0] != errno.ECONNRESET:
                         raise
+        
+    def write(self, *things):
+        self.respond('200 Script output follows', *things)
 
     def writelines(self, lines):
         for line in lines:
@@ -84,3 +84,9 @@
         if length:
             headers.append(('Content-length', str(length)))
         self.header(headers)
+
+def wsgiapplication(app_maker):
+	application = app_maker()
+	def run_wsgi(env, respond):
+		application(env, respond)
+	return run_wsgi
--- a/mercurial/hgweb/server.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/hgweb/server.py	Tue Dec 04 23:28:10 2007 +0100
@@ -10,7 +10,6 @@
 from mercurial import ui, hg, util, templater
 from hgweb_mod import hgweb
 from hgwebdir_mod import hgwebdir
-from request import wsgiapplication
 from mercurial.i18n import gettext as _
 
 def _splitURI(uri):
@@ -85,6 +84,7 @@
         env['SERVER_NAME'] = self.server.server_name
         env['SERVER_PORT'] = str(self.server.server_port)
         env['REQUEST_URI'] = self.path
+        env['SCRIPT_NAME'] = ''
         env['PATH_INFO'] = path_info
         env['REMOTE_HOST'] = self.client_address[0]
         env['REMOTE_ADDR'] = self.client_address[0]
@@ -121,10 +121,7 @@
         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)
+        self.server.application(env, self._start_response)
 
     def send_headers(self):
         if not self.saved_status:
@@ -250,7 +247,7 @@
                     raise hg.RepoError(_("There is no Mercurial repository here"
                                          " (.hg not found)"))
                 return hgwebobj
-            self.reqmaker = wsgiapplication(make_handler)
+            self.application = make_handler()
 
             addr = address
             if addr in ('', '::'):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/hgweb/webcommands.py	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,92 @@
+#
+# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
+# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+import os
+from mercurial import revlog
+from common import staticfile
+
+def log(web, req, tmpl):
+    if req.form.has_key('file') and req.form['file'][0]:
+        filelog(web, req, tmpl)
+    else:
+        changelog(web, req, tmpl)
+
+def file(web, req, tmpl):
+    path = web.cleanpath(req.form.get('file', [''])[0])
+    if path:
+        try:
+            req.write(web.filerevision(tmpl, web.filectx(req)))
+            return
+        except revlog.LookupError:
+            pass
+
+    req.write(web.manifest(tmpl, web.changectx(req), path))
+
+def changelog(web, req, tmpl, shortlog = False):
+    if req.form.has_key('node'):
+        ctx = web.changectx(req)
+    else:
+        if req.form.has_key('rev'):
+            hi = req.form['rev'][0]
+        else:
+            hi = web.repo.changelog.count() - 1
+        try:
+            ctx = web.repo.changectx(hi)
+        except hg.RepoError:
+            req.write(web.search(tmpl, hi)) # XXX redirect to 404 page?
+            return
+
+    req.write(web.changelog(tmpl, ctx, shortlog = shortlog))
+
+def shortlog(web, req, tmpl):
+    changelog(web, req, tmpl, shortlog = True)
+
+def changeset(web, req, tmpl):
+    req.write(web.changeset(tmpl, web.changectx(req)))
+
+rev = changeset
+
+def manifest(web, req, tmpl):
+    req.write(web.manifest(tmpl, web.changectx(req),
+                           web.cleanpath(req.form['path'][0])))
+
+def tags(web, req, tmpl):
+    req.write(web.tags(tmpl))
+
+def summary(web, req, tmpl):
+    req.write(web.summary(tmpl))
+
+def filediff(web, req, tmpl):
+    req.write(web.filediff(tmpl, web.filectx(req)))
+
+diff = filediff
+
+def annotate(web, req, tmpl):
+    req.write(web.fileannotate(tmpl, web.filectx(req)))
+
+def filelog(web, req, tmpl):
+    req.write(web.filelog(tmpl, web.filectx(req)))
+
+def archive(web, req, tmpl):
+    type_ = req.form['type'][0]
+    allowed = web.configlist("web", "allow_archive")
+    if (type_ in web.archives and (type_ in allowed or
+        web.configbool("web", "allow" + type_, False))):
+        web.archive(tmpl, req, req.form['node'][0], type_)
+        return
+
+    req.respond(400, tmpl('error',
+                           error='Unsupported archive type: %s' % type_))
+
+def static(web, req, tmpl):
+    fname = req.form['file'][0]
+    # a repo owner may set web.static in .hg/hgrc to get any file
+    # readable by the user running the CGI script
+    static = web.config("web", "static",
+                        os.path.join(web.templatepath, "static"),
+                        untrusted=False)
+    req.write(staticfile(static, fname, req))
--- a/mercurial/hgweb/wsgicgi.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/hgweb/wsgicgi.py	Tue Dec 04 23:28:10 2007 +0100
@@ -16,6 +16,7 @@
     util.set_binary(sys.stdout)
 
     environ = dict(os.environ.items())
+    environ.setdefault('PATH_INFO', '')
     environ['wsgi.input'] = sys.stdin
     environ['wsgi.errors'] = sys.stderr
     environ['wsgi.version'] = (1, 0)
@@ -61,13 +62,4 @@
         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()
+    application(environ, start_response)
--- a/mercurial/localrepo.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/localrepo.py	Tue Dec 04 23:28:10 2007 +0100
@@ -1710,6 +1710,8 @@
             # Go through all our files in order sorted by name.
             for fname in changedfiles:
                 filerevlog = self.file(fname)
+                if filerevlog.count() == 0:
+                    raise util.abort(_("empty or missing revlog for %s") % fname)
                 # Toss out the filenodes that the recipient isn't really
                 # missing.
                 if msng_filenode_set.has_key(fname):
@@ -1794,6 +1796,8 @@
 
             for fname in changedfiles:
                 filerevlog = self.file(fname)
+                if filerevlog.count() == 0:
+                    raise util.abort(_("empty or missing revlog for %s") % fname)
                 nodeiter = gennodelst(filerevlog)
                 nodeiter = list(nodeiter)
                 if nodeiter:
--- a/mercurial/revlog.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/revlog.py	Tue Dec 04 23:28:10 2007 +0100
@@ -31,8 +31,13 @@
 
 class RevlogError(Exception):
     pass
+
 class LookupError(RevlogError):
-    pass
+    def __init__(self, name, message=None):
+        if message is None:
+            message = _('not found: %s') % name
+        RevlogError.__init__(self, message)
+        self.name = name
 
 def getoffset(q):
     return int(q >> 16)
@@ -321,7 +326,7 @@
             e = _unpack(indexformatv0, cur)
             # transform to revlogv1 format
             e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
-                  nodemap[e[4]], nodemap[e[5]], e[6])
+                  nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
             index.append(e2)
             nodemap[e[6]] = n
             n += 1
@@ -516,7 +521,7 @@
         try:
             return self.nodemap[node]
         except KeyError:
-            raise LookupError(_('%s: no node %s') % (self.indexfile, hex(node)))
+            raise LookupError(hex(node), _('%s: no node %s') % (self.indexfile, hex(node)))
     def node(self, rev):
         return self.index[rev][7]
     def linkrev(self, node):
@@ -836,7 +841,8 @@
                 for n in self.nodemap:
                     if n.startswith(bin_id) and hex(n).startswith(id):
                         if node is not None:
-                            raise LookupError(_("Ambiguous identifier"))
+                            raise LookupError(hex(node),
+                                              _("Ambiguous identifier"))
                         node = n
                 if node is not None:
                     return node
@@ -855,7 +861,7 @@
         if n:
             return n
 
-        raise LookupError(_("No match found"))
+        raise LookupError(id, _("No match found"))
 
     def cmp(self, node, text):
         """compare text with a given file revision"""
@@ -1166,13 +1172,13 @@
 
             for p in (p1, p2):
                 if not p in self.nodemap:
-                    raise LookupError(_("unknown parent %s") % short(p))
+                    raise LookupError(hex(p), _("unknown parent %s") % short(p))
 
             if not chain:
                 # retrieve the parent revision of the delta chain
                 chain = p1
                 if not chain in self.nodemap:
-                    raise LookupError(_("unknown base %s") % short(chain[:4]))
+                    raise LookupError(hex(chain), _("unknown base %s") % short(chain[:4]))
 
             # full versions are inserted when the needed deltas become
             # comparable to the uncompressed text or when the previous
--- a/mercurial/util_win32.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/util_win32.py	Tue Dec 04 23:28:10 2007 +0100
@@ -16,6 +16,7 @@
 from i18n import _
 import errno, os, pywintypes, win32con, win32file, win32process
 import cStringIO, winerror
+import osutil
 from win32com.shell import shell,shellcon
 
 class WinError:
@@ -179,6 +180,20 @@
 
 def system_rcpath_win32():
     '''return default os-specific hgrc search path'''
+    try:
+        value = win32api.RegQueryValue(
+                win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Mercurial')
+        rcpath = []
+        for p in value.split(os.pathsep):
+            if p.lower().endswith('mercurial.ini'):
+                rcpath.append(p)
+            elif os.path.isdir(p):
+                for f, kind in osutil.listdir(p):
+                    if f.endswith('.rc'):
+                        rcpath.append(os.path.join(p, f))
+        return rcpath
+    except pywintypes.error:
+        pass
     proc = win32api.GetCurrentProcess()
     try:
         # This will fail on windows < NT
--- a/mercurial/verify.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/mercurial/verify.py	Tue Dec 04 23:28:10 2007 +0100
@@ -62,9 +62,14 @@
         repo.ui.status(_("repository uses revlog format %d\n") %
                        (revlogv1 and 1 or 0))
 
+    havecl = havemf = 1
     seen = {}
     repo.ui.status(_("checking changesets\n"))
-    checksize(repo.changelog, "changelog")
+    if repo.changelog.count() == 0 and repo.manifest.count() > 1:
+        havecl = 0
+        err(0, _("empty or missing 00changelog.i"))
+    else:
+        checksize(repo.changelog, "changelog")
 
     for i in xrange(repo.changelog.count()):
         changesets += 1
@@ -96,14 +101,18 @@
 
     seen = {}
     repo.ui.status(_("checking manifests\n"))
-    checkversion(repo.manifest, "manifest")
-    checksize(repo.manifest, "manifest")
+    if repo.changelog.count() > 0 and repo.manifest.count() == 0:
+        havemf = 0
+        err(0, _("empty or missing 00manifest.i"))
+    else:
+        checkversion(repo.manifest, "manifest")
+        checksize(repo.manifest, "manifest")
 
     for i in xrange(repo.manifest.count()):
         n = repo.manifest.node(i)
         l = repo.manifest.linkrev(n)
 
-        if l < 0 or l >= repo.changelog.count():
+        if l < 0 or (havecl and l >= repo.changelog.count()):
             err(None, _("bad link (%d) at manifest revision %d") % (l, i))
 
         if n in neededmanifests:
@@ -132,38 +141,51 @@
 
     repo.ui.status(_("crosschecking files in changesets and manifests\n"))
 
-    nm = neededmanifests.items()
-    nm.sort()
-    for m, c in nm:
-        err(m, _("changeset refers to unknown manifest %s") % short(c))
-    del neededmanifests, nm
+    if havemf > 0:
+        nm = [(c, m) for m, c in neededmanifests.items()]
+        nm.sort()
+        for c, m in nm:
+            err(c, _("changeset refers to unknown manifest %s") % short(m))
+        del neededmanifests, nm
 
-    for f in filenodes:
-        if f not in filelinkrevs:
-            lrs = [repo.manifest.linkrev(n) for n in filenodes[f]]
-            lrs.sort()
-            err(lrs[0], _("in manifest but not in changeset"), f)
+    if havecl:
+        fl = filenodes.keys()
+        fl.sort()
+        for f in fl:
+            if f not in filelinkrevs:
+                lrs = [repo.manifest.linkrev(n) for n in filenodes[f]]
+                lrs.sort()
+                err(lrs[0], _("in manifest but not in changeset"), f)
+        del fl
 
-    for f in filelinkrevs:
-        if f not in filenodes:
-            lr = filelinkrevs[f][0]
-            err(lr, _("in changeset but not in manifest"), f)
+    if havemf:
+        fl = filelinkrevs.keys()
+        fl.sort()
+        for f in fl:
+            if f not in filenodes:
+                lr = filelinkrevs[f][0]
+                err(lr, _("in changeset but not in manifest"), f)
+        del fl
 
     repo.ui.status(_("checking files\n"))
-    ff = filenodes.keys()
+    ff = dict.fromkeys(filenodes.keys() + filelinkrevs.keys()).keys()
     ff.sort()
     for f in ff:
         if f == "/dev/null":
             continue
         files += 1
         if not f:
-            lr = repo.manifest.linkrev(filenodes[f][0])
-            err(lr, _("file without name in manifest %s") % short(ff[n]))
+            lr = filelinkrevs[f][0]
+            err(lr, _("file without name in manifest"))
             continue
         fl = repo.file(f)
         checkversion(fl, f)
         checksize(fl, f)
 
+        if fl.count() == 0:
+            err(filelinkrevs[f][0], _("empty or missing revlog"), f)
+            continue
+
         seen = {}
         nodes = {nullid: 1}
         for i in xrange(fl.count()):
@@ -171,7 +193,7 @@
             n = fl.node(i)
             flr = fl.linkrev(n)
 
-            if flr not in filelinkrevs.get(f, []):
+            if flr < 0 or (havecl and flr not in filelinkrevs.get(f, [])):
                 if flr < 0 or flr >= repo.changelog.count():
                     err(None, _("rev %d point to nonexistent changeset %d")
                         % (i, flr), f)
@@ -182,14 +204,16 @@
                     warn(_(" (expected %s)") % filelinkrevs[f][0])
                 flr = None # can't be trusted
             else:
-                filelinkrevs[f].remove(flr)
+                if havecl:
+                    filelinkrevs[f].remove(flr)
 
             if n in seen:
                 err(flr, _("duplicate revision %d") % i, f)
-            if n not in filenodes[f]:
-                err(flr, _("%s not in manifests") % (short(n)), f)
-            else:
-                del filenodes[f][n]
+            if f in filenodes:
+                if havemf and n not in filenodes[f]:
+                    err(flr, _("%s not in manifests") % (short(n)), f)
+                else:
+                    del filenodes[f][n]
 
             # verify contents
             try:
@@ -230,11 +254,12 @@
                     (short(n), inst), f)
 
         # cross-check
-        fns = [(repo.manifest.linkrev(filenodes[f][n]), n)
-               for n in filenodes[f]]
-        fns.sort()
-        for lr, node in fns:
-            err(lr, _("%s in manifests not found") % short(node), f)
+        if f in filenodes:
+            fns = [(repo.manifest.linkrev(filenodes[f][n]), n)
+                   for n in filenodes[f]]
+            fns.sort()
+            for lr, node in fns:
+                err(lr, _("%s in manifests not found") % short(node), f)
 
     repo.ui.status(_("%d files, %d changesets, %d total revisions\n") %
                    (files, changesets, revisions))
@@ -247,4 +272,3 @@
             repo.ui.warn(_("(first damaged changeset appears to be %d)\n")
                          % firstbad[0])
         return 1
-
--- a/templates/gitweb/notfound.tmpl	Tue Dec 04 23:10:13 2007 +0100
+++ b/templates/gitweb/notfound.tmpl	Tue Dec 04 23:28:10 2007 +0100
@@ -1,5 +1,5 @@
 {header}
-<title>Mercurial repositories index</title>
+<title>Mercurial repository not found</title>
 </head>
 
 <body>
--- a/templates/map-cmdline.default	Tue Dec 04 23:10:13 2007 +0100
+++ b/templates/map-cmdline.default	Tue Dec 04 23:28:10 2007 +0100
@@ -1,10 +1,13 @@
 changeset = 'changeset:   {rev}:{node|short}\n{branches}{tags}{parents}user:        {author}\ndate:        {date|date}\nsummary:     {desc|firstline}\n\n'
 changeset_quiet = '{rev}:{node|short}\n'
-changeset_verbose = 'changeset:   {rev}:{node|short}\n{branches}{tags}{parents}{manifest}user:        {author}\ndate:        {date|date}\n{files}{file_adds}{file_dels}{file_copies}description:\n{desc|strip}\n\n\n'
-changeset_debug = 'changeset:   {rev}:{node}\n{branches}{tags}{parents}{manifest}user:        {author}\ndate:        {date|date}\n{files}{file_adds}{file_dels}{file_copies}{extras}description:\n{desc|strip}\n\n\n'
+changeset_verbose = 'changeset:   {rev}:{node|short}\n{branches}{tags}{parents}user:        {author}\ndate:        {date|date}\n{files}{file_copies}description:\n{desc|strip}\n\n\n'
+changeset_debug = 'changeset:   {rev}:{node}\n{branches}{tags}{parents}{manifest}user:        {author}\ndate:        {date|date}\n{file_mods}{file_adds}{file_dels}{file_copies}{extras}description:\n{desc|strip}\n\n\n'
 start_files = 'files:      '
 file = ' {file}'
 end_files = '\n'
+start_file_mods = 'files:      '
+file_mod = ' {file_mod}'
+end_file_mods = '\n'
 start_file_adds = 'files+:     '
 file_add = ' {file_add}'
 end_file_adds = '\n'
--- a/templates/notfound.tmpl	Tue Dec 04 23:10:13 2007 +0100
+++ b/templates/notfound.tmpl	Tue Dec 04 23:28:10 2007 +0100
@@ -1,9 +1,9 @@
 #header#
-<title>Mercurial repositories index</title>
+<title>Mercurial repository not found</title>
 </head>
 <body>
 
-<h2>Mercurial Repositories</h2>
+<h2>Mercurial repository not found</h2>
 
 The specified repository "#repo|escape#" is unknown, sorry.
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/raw/error.tmpl	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,2 @@
+#header#
+error: #error#
--- a/templates/raw/map	Tue Dec 04 23:10:13 2007 +0100
+++ b/templates/raw/map	Tue Dec 04 23:28:10 2007 +0100
@@ -18,4 +18,6 @@
 manifestdirentry = 'drwxr-xr-x {basename}\n'
 manifestfileentry = '{permissions|permissions} {size} {basename}\n'
 index = index.tmpl
+notfound = notfound.tmpl
+error = error.tmpl
 indexentry = '#url#\n'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/raw/notfound.tmpl	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,2 @@
+#header#
+error: repository #repo# not found
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/static/highlight.css	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,59 @@
+.c { color: #808080 } /* Comment */
+.err { color: #F00000; background-color: #F0A0A0 } /* Error */
+.k { color: #008000; font-weight: bold } /* Keyword */
+.o { color: #303030 } /* Operator */
+.cm { color: #808080 } /* Comment.Multiline */
+.cp { color: #507090 } /* Comment.Preproc */
+.c1 { color: #808080 } /* Comment.Single */
+.cs { color: #cc0000; font-weight: bold } /* Comment.Special */
+.gd { color: #A00000 } /* Generic.Deleted */
+.ge { font-style: italic } /* Generic.Emph */
+.gr { color: #FF0000 } /* Generic.Error */
+.gh { color: #000080; font-weight: bold } /* Generic.Heading */
+.gi { color: #00A000 } /* Generic.Inserted */
+.go { color: #808080 } /* Generic.Output */
+.gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
+.gs { font-weight: bold } /* Generic.Strong */
+.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
+.gt { color: #0040D0 } /* Generic.Traceback */
+.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
+.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
+.kp { color: #003080; font-weight: bold } /* Keyword.Pseudo */
+.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
+.kt { color: #303090; font-weight: bold } /* Keyword.Type */
+.m { color: #6000E0; font-weight: bold } /* Literal.Number */
+.s { background-color: #fff0f0 } /* Literal.String */
+.na { color: #0000C0 } /* Name.Attribute */
+.nb { color: #007020 } /* Name.Builtin */
+.nc { color: #B00060; font-weight: bold } /* Name.Class */
+.no { color: #003060; font-weight: bold } /* Name.Constant */
+.nd { color: #505050; font-weight: bold } /* Name.Decorator */
+.ni { color: #800000; font-weight: bold } /* Name.Entity */
+.ne { color: #F00000; font-weight: bold } /* Name.Exception */
+.nf { color: #0060B0; font-weight: bold } /* Name.Function */
+.nl { color: #907000; font-weight: bold } /* Name.Label */
+.nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
+.nt { color: #007000 } /* Name.Tag */
+.nv { color: #906030 } /* Name.Variable */
+.ow { color: #000000; font-weight: bold } /* Operator.Word */
+.w { color: #bbbbbb } /* Text.Whitespace */
+.mf { color: #6000E0; font-weight: bold } /* Literal.Number.Float */
+.mh { color: #005080; font-weight: bold } /* Literal.Number.Hex */
+.mi { color: #0000D0; font-weight: bold } /* Literal.Number.Integer */
+.mo { color: #4000E0; font-weight: bold } /* Literal.Number.Oct */
+.sb { background-color: #fff0f0 } /* Literal.String.Backtick */
+.sc { color: #0040D0 } /* Literal.String.Char */
+.sd { color: #D04020 } /* Literal.String.Doc */
+.s2 { background-color: #fff0f0 } /* Literal.String.Double */
+.se { color: #606060; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */
+.sh { background-color: #fff0f0 } /* Literal.String.Heredoc */
+.si { background-color: #e0e0e0 } /* Literal.String.Interpol */
+.sx { color: #D02000; background-color: #fff0f0 } /* Literal.String.Other */
+.sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */
+.s1 { background-color: #fff0f0 } /* Literal.String.Single */
+.ss { color: #A06000 } /* Literal.String.Symbol */
+.bp { color: #007020 } /* Name.Builtin.Pseudo */
+.vc { color: #306090 } /* Name.Variable.Class */
+.vg { color: #d07000; font-weight: bold } /* Name.Variable.Global */
+.vi { color: #3030B0 } /* Name.Variable.Instance */
+.il { color: #0000D0; font-weight: bold } /* Literal.Number.Integer.Long */
--- a/tests/coverage.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/coverage.py	Tue Dec 04 23:28:10 2007 +0100
@@ -22,15 +22,20 @@
 # interface and limitations.  See [GDR 2001-12-04b] for requirements and
 # design.
 
-"""Usage:
+r"""Usage:
 
-coverage.py -x MODULE.py [ARG1 ARG2 ...]
+coverage.py -x [-p] MODULE.py [ARG1 ARG2 ...]
     Execute module, passing the given command-line arguments, collecting
-    coverage data.
+    coverage data. With the -p option, write to a temporary file containing
+    the machine name and process ID.
 
 coverage.py -e
     Erase collected coverage data.
 
+coverage.py -c
+    Collect data from multiple coverage files (as created by -p option above)
+    and store it into a single file representing the union of the coverage.
+
 coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
     Report on the statement coverage for the given files.  With the -m
     option, show line numbers of the statements that weren't executed.
@@ -49,16 +54,26 @@
 Coverage data is saved in the file .coverage by default.  Set the
 COVERAGE_FILE environment variable to save it somewhere else."""
 
-__version__ = "2.5.20051204"    # see detailed history at the end of this file.
+__version__ = "2.77.20070729"    # see detailed history at the end of this file.
 
 import compiler
 import compiler.visitor
+import glob
 import os
 import re
 import string
+import symbol
 import sys
 import threading
+import token
 import types
+from socket import gethostname
+
+# Python version compatibility
+try:
+    strclass = basestring   # new to 2.3
+except:
+    strclass = str
 
 # 2. IMPLEMENTATION
 #
@@ -81,25 +96,29 @@
 # names to increase speed.
 
 class StatementFindingAstVisitor(compiler.visitor.ASTVisitor):
+    """ A visitor for a parsed Abstract Syntax Tree which finds executable
+        statements.
+    """
     def __init__(self, statements, excluded, suite_spots):
         compiler.visitor.ASTVisitor.__init__(self)
         self.statements = statements
         self.excluded = excluded
         self.suite_spots = suite_spots
         self.excluding_suite = 0
-
+        
     def doRecursive(self, node):
-        self.recordNodeLine(node)
         for n in node.getChildNodes():
             self.dispatch(n)
 
     visitStmt = visitModule = doRecursive
-
+    
     def doCode(self, node):
         if hasattr(node, 'decorators') and node.decorators:
             self.dispatch(node.decorators)
-        self.doSuite(node, node.code)
-
+            self.recordAndDispatch(node.code)
+        else:
+            self.doSuite(node, node.code)
+            
     visitFunction = visitClass = doCode
 
     def getFirstLine(self, node):
@@ -119,17 +138,40 @@
         for n in node.getChildNodes():
             lineno = max(lineno, self.getLastLine(n))
         return lineno
-
+    
     def doStatement(self, node):
         self.recordLine(self.getFirstLine(node))
 
-    visitAssert = visitAssign = visitAssTuple = visitDiscard = visitPrint = \
+    visitAssert = visitAssign = visitAssTuple = visitPrint = \
         visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
         doStatement
+    
+    def visitPass(self, node):
+        # Pass statements have weird interactions with docstrings.  If this
+        # pass statement is part of one of those pairs, claim that the statement
+        # is on the later of the two lines.
+        l = node.lineno
+        if l:
+            lines = self.suite_spots.get(l, [l,l])
+            self.statements[lines[1]] = 1
+        
+    def visitDiscard(self, node):
+        # Discard nodes are statements that execute an expression, but then
+        # discard the results.  This includes function calls, so we can't 
+        # ignore them all.  But if the expression is a constant, the statement
+        # won't be "executed", so don't count it now.
+        if node.expr.__class__.__name__ != 'Const':
+            self.doStatement(node)
 
     def recordNodeLine(self, node):
-        return self.recordLine(node.lineno)
-
+        # Stmt nodes often have None, but shouldn't claim the first line of
+        # their children (because the first child might be an ignorable line
+        # like "global a").
+        if node.__class__.__name__ != 'Stmt':
+            return self.recordLine(self.getFirstLine(node))
+        else:
+            return 0
+    
     def recordLine(self, lineno):
         # Returns a bool, whether the line is included or excluded.
         if lineno:
@@ -137,7 +179,7 @@
             # keyword.
             if lineno in self.suite_spots:
                 lineno = self.suite_spots[lineno][0]
-            # If we're inside an exluded suite, record that this line was
+            # If we're inside an excluded suite, record that this line was
             # excluded.
             if self.excluding_suite:
                 self.excluded[lineno] = 1
@@ -153,9 +195,9 @@
                 self.statements[lineno] = 1
                 return 1
         return 0
-
+    
     default = recordNodeLine
-
+    
     def recordAndDispatch(self, node):
         self.recordNodeLine(node)
         self.dispatch(node)
@@ -166,7 +208,7 @@
             self.excluding_suite = 1
         self.recordAndDispatch(body)
         self.excluding_suite = exsuite
-
+        
     def doPlainWordSuite(self, prevsuite, suite):
         # Finding the exclude lines for else's is tricky, because they aren't
         # present in the compiler parse tree.  Look at the previous suite,
@@ -180,15 +222,17 @@
                 break
         else:
             self.doSuite(None, suite)
-
+        
     def doElse(self, prevsuite, node):
         if node.else_:
             self.doPlainWordSuite(prevsuite, node.else_)
-
+    
     def visitFor(self, node):
         self.doSuite(node, node.body)
         self.doElse(node.body, node)
 
+    visitWhile = visitFor
+
     def visitIf(self, node):
         # The first test has to be handled separately from the rest.
         # The first test is credited to the line with the "if", but the others
@@ -198,10 +242,6 @@
             self.doSuite(t, n)
         self.doElse(node.tests[-1][1], node)
 
-    def visitWhile(self, node):
-        self.doSuite(node, node.body)
-        self.doElse(node.body, node)
-
     def visitTryExcept(self, node):
         self.doSuite(node, node.body)
         for i in range(len(node.handlers)):
@@ -216,11 +256,14 @@
             else:
                 self.doSuite(a, h)
         self.doElse(node.handlers[-1][2], node)
-
+    
     def visitTryFinally(self, node):
         self.doSuite(node, node.body)
         self.doPlainWordSuite(node.body, node.final)
-
+        
+    def visitWith(self, node):
+        self.doSuite(node, node.body)
+        
     def visitGlobal(self, node):
         # "global" statements don't execute like others (they don't call the
         # trace function), so don't record their line numbers.
@@ -228,9 +271,9 @@
 
 the_coverage = None
 
+class CoverageException(Exception): pass
+
 class coverage:
-    error = "coverage error"
-
     # Name of the cache file (unless environment variable is set).
     cache_default = ".coverage"
 
@@ -240,7 +283,7 @@
     # A dictionary with an entry for (Python source file name, line number
     # in that file) if that line has been executed.
     c = {}
-
+    
     # A map from canonical Python source file name to a dictionary in
     # which there's an entry for each line number that has been
     # executed.
@@ -257,53 +300,58 @@
     def __init__(self):
         global the_coverage
         if the_coverage:
-            raise self.error, "Only one coverage object allowed."
+            raise CoverageException, "Only one coverage object allowed."
         self.usecache = 1
         self.cache = None
+        self.parallel_mode = False
         self.exclude_re = ''
         self.nesting = 0
         self.cstack = []
         self.xstack = []
-        self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.path.sep)
+        self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.sep)
+        self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
 
-    # t(f, x, y).  This method is passed to sys.settrace as a trace function.
-    # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and
+    # t(f, x, y).  This method is passed to sys.settrace as a trace function.  
+    # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and 
     # the arguments and return value of the trace function.
     # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code
     # objects.
-
-    def t(self, f, w, a):                                   #pragma: no cover
-        #print w, f.f_code.co_filename, f.f_lineno
+    
+    def t(self, f, w, unused):                                   #pragma: no cover
         if w == 'line':
+            #print "Executing %s @ %d" % (f.f_code.co_filename, f.f_lineno)
             self.c[(f.f_code.co_filename, f.f_lineno)] = 1
             for c in self.cstack:
                 c[(f.f_code.co_filename, f.f_lineno)] = 1
         return self.t
-
-    def help(self, error=None):
+    
+    def help(self, error=None):     #pragma: no cover
         if error:
             print error
             print
         print __doc__
         sys.exit(1)
 
-    def command_line(self):
+    def command_line(self, argv, help_fn=None):
         import getopt
+        help_fn = help_fn or self.help
         settings = {}
         optmap = {
             '-a': 'annotate',
+            '-c': 'collect',
             '-d:': 'directory=',
             '-e': 'erase',
             '-h': 'help',
             '-i': 'ignore-errors',
             '-m': 'show-missing',
+            '-p': 'parallel-mode',
             '-r': 'report',
             '-x': 'execute',
-            '-o': 'omit=',
+            '-o:': 'omit=',
             }
         short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
         long_opts = optmap.values()
-        options, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
+        options, args = getopt.getopt(argv, short_opts, long_opts)
         for o, a in options:
             if optmap.has_key(o):
                 settings[optmap[o]] = 1
@@ -312,69 +360,84 @@
             elif o[2:] in long_opts:
                 settings[o[2:]] = 1
             elif o[2:] + '=' in long_opts:
-                settings[o[2:]] = a
-            else:
-                self.help("Unknown option: '%s'." % o)
+                settings[o[2:]+'='] = a
+            else:       #pragma: no cover
+                pass    # Can't get here, because getopt won't return anything unknown.
+
         if settings.get('help'):
-            self.help()
+            help_fn()
+
         for i in ['erase', 'execute']:
-            for j in ['annotate', 'report']:
+            for j in ['annotate', 'report', 'collect']:
                 if settings.get(i) and settings.get(j):
-                    self.help("You can't specify the '%s' and '%s' "
+                    help_fn("You can't specify the '%s' and '%s' "
                               "options at the same time." % (i, j))
+
         args_needed = (settings.get('execute')
                        or settings.get('annotate')
                        or settings.get('report'))
-        action = settings.get('erase') or args_needed
+        action = (settings.get('erase') 
+                  or settings.get('collect')
+                  or args_needed)
         if not action:
-            self.help("You must specify at least one of -e, -x, -r, or -a.")
+            help_fn("You must specify at least one of -e, -x, -c, -r, or -a.")
         if not args_needed and args:
-            self.help("Unexpected arguments %s." % args)
-
+            help_fn("Unexpected arguments: %s" % " ".join(args))
+        
+        self.parallel_mode = settings.get('parallel-mode')
         self.get_ready()
-        self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
 
         if settings.get('erase'):
             self.erase()
         if settings.get('execute'):
             if not args:
-                self.help("Nothing to do.")
+                help_fn("Nothing to do.")
             sys.argv = args
             self.start()
             import __main__
             sys.path[0] = os.path.dirname(sys.argv[0])
             execfile(sys.argv[0], __main__.__dict__)
+        if settings.get('collect'):
+            self.collect()
         if not args:
             args = self.cexecuted.keys()
+        
         ignore_errors = settings.get('ignore-errors')
         show_missing = settings.get('show-missing')
-        directory = settings.get('directory')
-        omit = filter(None, settings.get('omit', '').split(','))
-        omit += ['/<'] # Always skip /<string> etc.
+        directory = settings.get('directory=')
+
+        omit = settings.get('omit=')
+        if omit is not None:
+            omit = omit.split(',')
+        else:
+            omit = []
 
         if settings.get('report'):
             self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
         if settings.get('annotate'):
             self.annotate(args, directory, ignore_errors, omit_prefixes=omit)
 
-    def use_cache(self, usecache):
+    def use_cache(self, usecache, cache_file=None):
         self.usecache = usecache
-
-    def get_ready(self):
+        if cache_file and not self.cache:
+            self.cache_default = cache_file
+        
+    def get_ready(self, parallel_mode=False):
         if self.usecache and not self.cache:
-            self.cache = os.path.abspath(os.environ.get(self.cache_env,
-                                                        self.cache_default))
+            self.cache = os.environ.get(self.cache_env, self.cache_default)
+            if self.parallel_mode:
+                self.cache += "." + gethostname() + "." + str(os.getpid())
             self.restore()
         self.analysis_cache = {}
-
-    def start(self):
+        
+    def start(self, parallel_mode=False):
         self.get_ready()
         if self.nesting == 0:                               #pragma: no cover
             sys.settrace(self.t)
             if hasattr(threading, 'settrace'):
                 threading.settrace(self.t)
         self.nesting += 1
-
+        
     def stop(self):
         self.nesting -= 1
         if self.nesting == 0:                               #pragma: no cover
@@ -383,12 +446,12 @@
                 threading.settrace(None)
 
     def erase(self):
+        self.get_ready()
         self.c = {}
         self.analysis_cache = {}
         self.cexecuted = {}
         if self.cache and os.path.exists(self.cache):
             os.remove(self.cache)
-        self.exclude_re = ""
 
     def exclude(self, re):
         if self.exclude_re:
@@ -398,7 +461,7 @@
     def begin_recursive(self):
         self.cstack.append(self.c)
         self.xstack.append(self.exclude_re)
-
+        
     def end_recursive(self):
         self.c = self.cstack.pop()
         self.exclude_re = self.xstack.pop()
@@ -406,8 +469,6 @@
     # save().  Save coverage data to the coverage cache.
 
     def save(self):
-        # move to directory that must exist.
-        os.chdir(os.sep)
         if self.usecache and self.cache:
             self.canonicalize_filenames()
             cache = open(self.cache, 'wb')
@@ -421,17 +482,45 @@
         self.c = {}
         self.cexecuted = {}
         assert self.usecache
-        if not os.path.exists(self.cache):
-            return
+        if os.path.exists(self.cache):
+            self.cexecuted = self.restore_file(self.cache)
+
+    def restore_file(self, file_name):
         try:
-            cache = open(self.cache, 'rb')
+            cache = open(file_name, 'rb')
             import marshal
             cexecuted = marshal.load(cache)
             cache.close()
             if isinstance(cexecuted, types.DictType):
-                self.cexecuted = cexecuted
+                return cexecuted
+            else:
+                return {}
         except:
-            pass
+            return {}
+
+    # collect(). Collect data in multiple files produced by parallel mode
+
+    def collect(self):
+        cache_dir, local = os.path.split(self.cache)
+        for f in os.listdir(cache_dir or '.'):
+            if not f.startswith(local):
+                continue
+
+            full_path = os.path.join(cache_dir, f)
+            cexecuted = self.restore_file(full_path)
+            self.merge_data(cexecuted)
+
+    def merge_data(self, new_data):
+        for file_name, file_data in new_data.items():
+            if self.cexecuted.has_key(file_name):
+                self.merge_file_data(self.cexecuted[file_name], file_data)
+            else:
+                self.cexecuted[file_name] = file_data
+
+    def merge_file_data(self, cache_data, new_data):
+        for line_number in new_data.keys():
+            if not cache_data.has_key(line_number):
+                cache_data[line_number] = new_data[line_number]
 
     # canonical_filename(filename).  Return a canonical filename for the
     # file (that is, an absolute path with no redundant components and
@@ -452,11 +541,14 @@
             self.canonical_filename_cache[filename] = cf
         return self.canonical_filename_cache[filename]
 
-    # canonicalize_filenames().  Copy results from "c" to "cexecuted",
+    # canonicalize_filenames().  Copy results from "c" to "cexecuted", 
     # canonicalizing filenames on the way.  Clear the "c" map.
 
     def canonicalize_filenames(self):
         for filename, lineno in self.c.keys():
+            if filename == '<string>':
+                # Can't do anything useful with exec'd strings, so skip them.
+                continue
             f = self.canonical_filename(filename)
             if not self.cexecuted.has_key(f):
                 self.cexecuted[f] = {}
@@ -468,18 +560,20 @@
     def morf_filename(self, morf):
         if isinstance(morf, types.ModuleType):
             if not hasattr(morf, '__file__'):
-                raise self.error, "Module has no __file__ attribute."
-            file = morf.__file__
+                raise CoverageException, "Module has no __file__ attribute."
+            f = morf.__file__
         else:
-            file = morf
-        return self.canonical_filename(file)
+            f = morf
+        return self.canonical_filename(f)
 
     # analyze_morf(morf).  Analyze the module or filename passed as
     # the argument.  If the source code can't be found, raise an error.
     # Otherwise, return a tuple of (1) the canonical filename of the
     # source code for the module, (2) a list of lines of statements
-    # in the source code, and (3) a list of lines of excluded statements.
-
+    # in the source code, (3) a list of lines of excluded statements,
+    # and (4), a map of line numbers to multi-line line number ranges, for
+    # statements that cross lines.
+    
     def analyze_morf(self, morf):
         if self.analysis_cache.has_key(morf):
             return self.analysis_cache[morf]
@@ -487,30 +581,69 @@
         ext = os.path.splitext(filename)[1]
         if ext == '.pyc':
             if not os.path.exists(filename[0:-1]):
-                raise self.error, ("No source for compiled code '%s'."
+                raise CoverageException, ("No source for compiled code '%s'."
                                    % filename)
             filename = filename[0:-1]
         elif ext != '.py':
-            raise self.error, "File '%s' not Python source." % filename
+            raise CoverageException, "File '%s' not Python source." % filename
         source = open(filename, 'r')
-        lines, excluded_lines = self.find_executable_statements(
+        lines, excluded_lines, line_map = self.find_executable_statements(
             source.read(), exclude=self.exclude_re
             )
         source.close()
-        result = filename, lines, excluded_lines
+        result = filename, lines, excluded_lines, line_map
         self.analysis_cache[morf] = result
         return result
 
+    def first_line_of_tree(self, tree):
+        while True:
+            if len(tree) == 3 and type(tree[2]) == type(1):
+                return tree[2]
+            tree = tree[1]
+    
+    def last_line_of_tree(self, tree):
+        while True:
+            if len(tree) == 3 and type(tree[2]) == type(1):
+                return tree[2]
+            tree = tree[-1]
+    
+    def find_docstring_pass_pair(self, tree, spots):
+        for i in range(1, len(tree)):
+            if self.is_string_constant(tree[i]) and self.is_pass_stmt(tree[i+1]):
+                first_line = self.first_line_of_tree(tree[i])
+                last_line = self.last_line_of_tree(tree[i+1])
+                self.record_multiline(spots, first_line, last_line)
+        
+    def is_string_constant(self, tree):
+        try:
+            return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.expr_stmt
+        except:
+            return False
+        
+    def is_pass_stmt(self, tree):
+        try:
+            return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.pass_stmt
+        except:
+            return False
+
+    def record_multiline(self, spots, i, j):
+        for l in range(i, j+1):
+            spots[l] = (i, j)
+            
     def get_suite_spots(self, tree, spots):
-        import symbol, token
+        """ Analyze a parse tree to find suite introducers which span a number
+            of lines.
+        """
         for i in range(1, len(tree)):
-            if isinstance(tree[i], tuple):
+            if type(tree[i]) == type(()):
                 if tree[i][0] == symbol.suite:
                     # Found a suite, look back for the colon and keyword.
                     lineno_colon = lineno_word = None
                     for j in range(i-1, 0, -1):
                         if tree[j][0] == token.COLON:
-                            lineno_colon = tree[j][2]
+                            # Colons are never executed themselves: we want the
+                            # line number of the last token before the colon.
+                            lineno_colon = self.last_line_of_tree(tree[j-1])
                         elif tree[j][0] == token.NAME:
                             if tree[j][1] == 'elif':
                                 # Find the line number of the first non-terminal
@@ -532,8 +665,18 @@
                     if lineno_colon and lineno_word:
                         # Found colon and keyword, mark all the lines
                         # between the two with the two line numbers.
-                        for l in range(lineno_word, lineno_colon+1):
-                            spots[l] = (lineno_word, lineno_colon)
+                        self.record_multiline(spots, lineno_word, lineno_colon)
+
+                    # "pass" statements are tricky: different versions of Python
+                    # treat them differently, especially in the common case of a
+                    # function with a doc string and a single pass statement.
+                    self.find_docstring_pass_pair(tree[i], spots)
+                    
+                elif tree[i][0] == symbol.simple_stmt:
+                    first_line = self.first_line_of_tree(tree[i])
+                    last_line = self.last_line_of_tree(tree[i])
+                    if first_line != last_line:
+                        self.record_multiline(spots, first_line, last_line)
                 self.get_suite_spots(tree[i], spots)
 
     def find_executable_statements(self, text, exclude=None):
@@ -547,10 +690,13 @@
                 if reExclude.search(lines[i]):
                     excluded[i+1] = 1
 
+        # Parse the code and analyze the parse tree to find out which statements
+        # are multiline, and where suites begin and end.
         import parser
         tree = parser.suite(text+'\n\n').totuple(1)
         self.get_suite_spots(tree, suite_spots)
-
+        #print "Suite spots:", suite_spots
+        
         # Use the compiler module to parse the text and find the executable
         # statements.  We add newlines to be impervious to final partial lines.
         statements = {}
@@ -562,7 +708,7 @@
         lines.sort()
         excluded_lines = excluded.keys()
         excluded_lines.sort()
-        return lines, excluded_lines
+        return lines, excluded_lines, suite_spots
 
     # format_lines(statements, lines).  Format a list of line numbers
     # for printing by coalescing groups of lines as long as the lines
@@ -595,7 +741,8 @@
                 return "%d" % start
             else:
                 return "%d-%d" % (start, end)
-        return string.join(map(stringify, pairs), ", ")
+        ret = string.join(map(stringify, pairs), ", ")
+        return ret
 
     # Backward compatibility with version 1.
     def analysis(self, morf):
@@ -603,13 +750,17 @@
         return f, s, m, mf
 
     def analysis2(self, morf):
-        filename, statements, excluded = self.analyze_morf(morf)
+        filename, statements, excluded, line_map = self.analyze_morf(morf)
         self.canonicalize_filenames()
         if not self.cexecuted.has_key(filename):
             self.cexecuted[filename] = {}
         missing = []
         for line in statements:
-            if not self.cexecuted[filename].has_key(line):
+            lines = line_map.get(line, [line, line])
+            for l in range(lines[0], lines[1]+1):
+                if self.cexecuted[filename].has_key(l):
+                    break
+            else:
                 missing.append(line)
         return (filename, statements, excluded, missing,
                 self.format_lines(statements, missing))
@@ -647,6 +798,15 @@
     def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]):
         if not isinstance(morfs, types.ListType):
             morfs = [morfs]
+        # On windows, the shell doesn't expand wildcards.  Do it here.
+        globbed = []
+        for morf in morfs:
+            if isinstance(morf, strclass):
+                globbed.extend(glob.glob(morf))
+            else:
+                globbed.append(morf)
+        morfs = globbed
+        
         morfs = self.filter_by_prefix(morfs, omit_prefixes)
         morfs.sort(self.morf_name_compare)
 
@@ -684,8 +844,8 @@
                 raise
             except:
                 if not ignore_errors:
-                    type, msg = sys.exc_info()[0:2]
-                    print >>file, fmt_err % (name, type, msg)
+                    typ, msg = sys.exc_info()[0:2]
+                    print >>file, fmt_err % (name, typ, msg)
         if len(morfs) > 1:
             print >>file, "-" * len(header)
             if total_statements > 0:
@@ -713,7 +873,7 @@
             except:
                 if not ignore_errors:
                     raise
-
+                
     def annotate_file(self, filename, statements, excluded, missing, directory=None):
         source = open(filename, 'r')
         if directory:
@@ -741,7 +901,7 @@
             if self.blank_re.match(line):
                 dest.write('  ')
             elif self.else_re.match(line):
-                # Special logic for lines containing only 'else:'.
+                # Special logic for lines containing only 'else:'.  
                 # See [GDR 2001-12-04b, 3.2].
                 if i >= len(statements) and j >= len(missing):
                     dest.write('! ')
@@ -765,18 +925,41 @@
 the_coverage = coverage()
 
 # Module functions call methods in the singleton object.
-def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw)
-def start(*args, **kw): return the_coverage.start(*args, **kw)
-def stop(*args, **kw): return the_coverage.stop(*args, **kw)
-def erase(*args, **kw): return the_coverage.erase(*args, **kw)
-def begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw)
-def end_recursive(*args, **kw): return the_coverage.end_recursive(*args, **kw)
-def exclude(*args, **kw): return the_coverage.exclude(*args, **kw)
-def analysis(*args, **kw): return the_coverage.analysis(*args, **kw)
-def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw)
-def report(*args, **kw): return the_coverage.report(*args, **kw)
-def annotate(*args, **kw): return the_coverage.annotate(*args, **kw)
-def annotate_file(*args, **kw): return the_coverage.annotate_file(*args, **kw)
+def use_cache(*args, **kw): 
+    return the_coverage.use_cache(*args, **kw)
+
+def start(*args, **kw): 
+    return the_coverage.start(*args, **kw)
+
+def stop(*args, **kw): 
+    return the_coverage.stop(*args, **kw)
+
+def erase(*args, **kw): 
+    return the_coverage.erase(*args, **kw)
+
+def begin_recursive(*args, **kw): 
+    return the_coverage.begin_recursive(*args, **kw)
+
+def end_recursive(*args, **kw): 
+    return the_coverage.end_recursive(*args, **kw)
+
+def exclude(*args, **kw): 
+    return the_coverage.exclude(*args, **kw)
+
+def analysis(*args, **kw): 
+    return the_coverage.analysis(*args, **kw)
+
+def analysis2(*args, **kw): 
+    return the_coverage.analysis2(*args, **kw)
+
+def report(*args, **kw): 
+    return the_coverage.report(*args, **kw)
+
+def annotate(*args, **kw): 
+    return the_coverage.annotate(*args, **kw)
+
+def annotate_file(*args, **kw): 
+    return the_coverage.annotate_file(*args, **kw)
 
 # Save coverage data when Python exits.  (The atexit module wasn't
 # introduced until Python 2.0, so use sys.exitfunc when it's not
@@ -789,7 +972,7 @@
 
 # Command-line interface.
 if __name__ == '__main__':
-    the_coverage.command_line()
+    the_coverage.command_line(sys.argv[1:])
 
 
 # A. REFERENCES
@@ -850,7 +1033,7 @@
 # Thanks, Allen.
 #
 # 2005-12-02 NMB Call threading.settrace so that all threads are measured.
-# Thanks Martin Fuzzey. Add a file argument to report so that reports can be
+# Thanks Martin Fuzzey. Add a file argument to report so that reports can be 
 # captured to a different destination.
 #
 # 2005-12-03 NMB coverage.py can now measure itself.
@@ -858,10 +1041,46 @@
 # 2005-12-04 NMB Adapted Greg Rogers' patch for using relative filenames,
 # and sorting and omitting files to report on.
 #
+# 2006-07-23 NMB Applied Joseph Tate's patch for function decorators.
+#
+# 2006-08-21 NMB Applied Sigve Tjora and Mark van der Wal's fixes for argument
+# handling.
+#
+# 2006-08-22 NMB Applied Geoff Bache's parallel mode patch.
+#
+# 2006-08-23 NMB Refactorings to improve testability.  Fixes to command-line
+# logic for parallel mode and collect.
+#
+# 2006-08-25 NMB "#pragma: nocover" is excluded by default.
+#
+# 2006-09-10 NMB Properly ignore docstrings and other constant expressions that
+# appear in the middle of a function, a problem reported by Tim Leslie.
+# Minor changes to avoid lint warnings.
+#
+# 2006-09-17 NMB coverage.erase() shouldn't clobber the exclude regex.
+# Change how parallel mode is invoked, and fix erase() so that it erases the
+# cache when called programmatically.
+#
+# 2007-07-21 NMB In reports, ignore code executed from strings, since we can't
+# do anything useful with it anyway.
+# Better file handling on Linux, thanks Guillaume Chazarain.
+# Better shell support on Windows, thanks Noel O'Boyle.
+# Python 2.2 support maintained, thanks Catherine Proulx.
+#
+# 2007-07-22 NMB Python 2.5 now fully supported. The method of dealing with
+# multi-line statements is now less sensitive to the exact line that Python
+# reports during execution. Pass statements are handled specially so that their
+# disappearance during execution won't throw off the measurement.
+#
+# 2007-07-23 NMB Now Python 2.5 is *really* fully supported: the body of the
+# new with statement is counted as executable.
+#
+# 2007-07-29 NMB Better packaging.
+
 # C. COPYRIGHT AND LICENCE
 #
 # Copyright 2001 Gareth Rees.  All rights reserved.
-# Copyright 2004-2005 Ned Batchelder.  All rights reserved.
+# Copyright 2004-2007 Ned Batchelder.  All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are
@@ -888,4 +1107,4 @@
 # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 # DAMAGE.
 #
-# $Id: coverage.py 26 2005-12-04 18:42:44Z ned $
+# $Id: coverage.py 74 2007-07-29 22:28:35Z nedbat $
--- a/tests/get-with-headers.py	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/get-with-headers.py	Tue Dec 04 23:28:10 2007 +0100
@@ -14,3 +14,7 @@
         print "%s: %s" % (h, response.getheader(h))
 print
 sys.stdout.write(response.read())
+
+if 200 <= response.status <= 299:
+    sys.exit(0)
+sys.exit(1)
--- a/tests/test-command-template	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-command-template	Tue Dec 04 23:28:10 2007 +0100
@@ -89,8 +89,8 @@
 cat changelog
 
 echo "# keys work"
-for key in author branches date desc file_adds file_dels files \
-        manifest node parents rev tags; do
+for key in author branches date desc file_adds file_dels file_mods \
+        files manifest node parents rev tags; do
     for mode in '' --verbose --debug; do
         hg log $mode --template "$key$mode: {$key}\n"
     done
--- a/tests/test-command-template.out	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-command-template.out	Tue Dec 04 23:28:10 2007 +0100
@@ -260,22 +260,22 @@
 other 3
 desc--debug: line 1
 line 2
-file_adds: 
-file_adds: 
+file_adds: second
 file_adds: 
-file_adds: 
-file_adds: 
-file_adds: 
+file_adds: d
 file_adds: 
 file_adds: 
+file_adds: c
+file_adds: b
+file_adds: a
+file_adds--verbose: second
 file_adds--verbose: 
-file_adds--verbose: 
+file_adds--verbose: d
 file_adds--verbose: 
 file_adds--verbose: 
-file_adds--verbose: 
-file_adds--verbose: 
-file_adds--verbose: 
-file_adds--verbose: 
+file_adds--verbose: c
+file_adds--verbose: b
+file_adds--verbose: a
 file_adds--debug: second
 file_adds--debug: 
 file_adds--debug: d
@@ -308,6 +308,30 @@
 file_dels--debug: 
 file_dels--debug: 
 file_dels--debug: 
+file_mods: 
+file_mods: 
+file_mods: 
+file_mods: 
+file_mods: c
+file_mods: 
+file_mods: 
+file_mods: 
+file_mods--verbose: 
+file_mods--verbose: 
+file_mods--verbose: 
+file_mods--verbose: 
+file_mods--verbose: c
+file_mods--verbose: 
+file_mods--verbose: 
+file_mods--verbose: 
+file_mods--debug: 
+file_mods--debug: 
+file_mods--debug: 
+file_mods--debug: 
+file_mods--debug: c
+file_mods--debug: 
+file_mods--debug: 
+file_mods--debug: 
 files: second
 files: 
 files: d
@@ -324,30 +348,30 @@
 files--verbose: c
 files--verbose: b
 files--verbose: a
-files--debug: 
+files--debug: second
 files--debug: 
-files--debug: 
+files--debug: d
 files--debug: 
 files--debug: c
-files--debug: 
-files--debug: 
-files--debug: 
-manifest: 
-manifest: 
-manifest: 
-manifest: 
-manifest: 
-manifest: 
-manifest: 
-manifest: 
-manifest--verbose: 
-manifest--verbose: 
-manifest--verbose: 
-manifest--verbose: 
-manifest--verbose: 
-manifest--verbose: 
-manifest--verbose: 
-manifest--verbose: 
+files--debug: c
+files--debug: b
+files--debug: a
+manifest: 7:f2dbc354b94e
+manifest: 6:91015e9dbdd7
+manifest: 5:4dc3def4f9b4
+manifest: 4:90ae8dda64e1
+manifest: 3:cb5a1327723b
+manifest: 2:6e0e82995c35
+manifest: 1:4e8d705b1e53
+manifest: 0:a0c8bcbbb45c
+manifest--verbose: 7:f2dbc354b94e
+manifest--verbose: 6:91015e9dbdd7
+manifest--verbose: 5:4dc3def4f9b4
+manifest--verbose: 4:90ae8dda64e1
+manifest--verbose: 3:cb5a1327723b
+manifest--verbose: 2:6e0e82995c35
+manifest--verbose: 1:4e8d705b1e53
+manifest--verbose: 0:a0c8bcbbb45c
 manifest--debug: 7:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf
 manifest--debug: 6:91015e9dbdd76a6791085d12b0a0ec7fcd22ffbf
 manifest--debug: 5:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
--- a/tests/test-convert	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-convert	Tue Dec 04 23:28:10 2007 +0100
@@ -1,7 +1,11 @@
 #!/bin/sh
 
-echo "[extensions]" >> $HGRCPATH
-echo "convert=" >> $HGRCPATH
+cat >> $HGRCPATH <<EOF
+[extensions]
+convert=
+[convert]
+hg.saverev=False
+EOF
 
 hg help convert
 
--- a/tests/test-convert-cvs.out	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-convert-cvs.out	Tue Dec 04 23:28:10 2007 +0100
@@ -44,7 +44,6 @@
 checking in src/a,v
 checking in src/b/c,v
 % convert again
-destination src-hg is a Mercurial repository
 connecting to cvsrepo
 scanning source...
 sorting...
@@ -56,7 +55,6 @@
 c
 c
 % convert again with --filemap
-destination src-filemap is a Mercurial repository
 connecting to cvsrepo
 scanning source...
 sorting...
--- a/tests/test-convert-hg-sink	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-convert-hg-sink	Tue Dec 04 23:28:10 2007 +0100
@@ -1,7 +1,11 @@
 #!/bin/sh
 
-echo "[extensions]" >> $HGRCPATH
-echo "hgext.convert=" >> $HGRCPATH
+cat >> $HGRCPATH <<EOF
+[extensions]
+convert=
+[convert]
+hg.saverev=False
+EOF
 
 hg init orig
 cd orig
--- a/tests/test-convert-hg-sink.out	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-convert-hg-sink.out	Tue Dec 04 23:28:10 2007 +0100
@@ -37,7 +37,6 @@
 a   0         -1 unset             baz
 copy: bar -> baz
 % add a new revision in the original repo
-destination new is a Mercurial repository
 scanning source...
 sorting...
 converting...
--- a/tests/test-convert-hg-source	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-convert-hg-source	Tue Dec 04 23:28:10 2007 +0100
@@ -1,7 +1,11 @@
 #!/bin/sh
 
-echo "[extensions]" >> $HGRCPATH
-echo "hgext.convert=" >> $HGRCPATH
+cat >> $HGRCPATH <<EOF
+[extensions]
+convert=
+[convert]
+hg.saverev=False
+EOF
 
 hg init orig
 cd orig
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-hg-svn	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+"$TESTDIR/hghave" svn svn-bindings || exit 80
+
+fix_path()
+{
+    tr '\\' /
+}
+
+echo "[extensions]" >> $HGRCPATH
+echo "convert = " >> $HGRCPATH
+
+svnpath=`pwd`/svn-repo
+svnadmin create $svnpath
+
+cat > $svnpath/hooks/pre-revprop-change <<'EOF'
+#!/bin/sh
+
+REPOS="$1"
+REV="$2"
+USER="$3"
+PROPNAME="$4"
+ACTION="$5"
+
+if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
+if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
+if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
+
+echo "Changing prohibited revision property" >&2
+exit 1
+EOF
+chmod +x $svnpath/hooks/pre-revprop-change
+
+svnurl=file://$svnpath
+svn co $svnurl $svnpath-wc
+
+cd $svnpath-wc
+echo a > a
+svn add a
+svn ci -m'added a' a
+
+cd ..
+
+echo % initial roundtrip
+hg convert -s svn -d hg $svnpath-wc $svnpath-hg | grep -v initializing
+hg convert -s hg -d svn $svnpath-hg $svnpath-wc
+
+echo % second roundtrip should do nothing
+hg convert -s svn -d hg $svnpath-wc $svnpath-hg
+hg convert -s hg -d svn $svnpath-hg $svnpath-wc
+
+echo % new hg rev
+
+hg clone $svnpath-hg $svnpath-work
+echo b > $svnpath-work/b
+hg --cwd $svnpath-work add b
+hg --cwd $svnpath-work ci -mb
+
+echo % echo hg to svn
+hg --cwd $svnpath-hg pull -q $svnpath-work
+hg convert -s hg -d svn $svnpath-hg $svnpath-wc
+
+echo % svn back to hg should do nothing
+hg convert -s svn -d hg $svnpath-wc $svnpath-hg
+echo % hg back to svn should do nothing
+hg convert -s hg -d svn $svnpath-hg $svnpath-wc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-hg-svn.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,35 @@
+Checked out revision 0.
+A         a
+Adding         a
+Transmitting file data .
+Committed revision 1.
+% initial roundtrip
+scanning source...
+sorting...
+converting...
+0 added a
+scanning source...
+sorting...
+converting...
+% second roundtrip should do nothing
+scanning source...
+sorting...
+converting...
+scanning source...
+sorting...
+converting...
+% new hg rev
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% echo hg to svn
+scanning source...
+sorting...
+converting...
+0 b
+% svn back to hg should do nothing
+scanning source...
+sorting...
+converting...
+% hg back to svn should do nothing
+scanning source...
+sorting...
+converting...
--- a/tests/test-convert-svn	Tue Dec 04 23:10:13 2007 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-#!/bin/sh
-
-"$TESTDIR/hghave" svn svn-bindings || exit 80
-
-fix_path()
-{
-    tr '\\' /
-}
-
-echo "[extensions]" >> $HGRCPATH
-echo "convert = " >> $HGRCPATH
-
-svnadmin create svn-repo
-
-echo % initial svn import
-mkdir t
-cd t
-echo a > a
-cd ..
-
-svnpath=`pwd | fix_path`
-# SVN wants all paths to start with a slash. Unfortunately,
-# Windows ones don't. Handle that.
-expr $svnpath : "\/" > /dev/null
-if [ $? -ne 0 ]; then
-    svnpath='/'$svnpath
-fi
-
-svnurl=file://$svnpath/svn-repo/trunk
-svn import -m init t $svnurl | fix_path
-
-echo % update svn repository
-svn co $svnurl t2 | fix_path
-cd t2
-echo b >> a
-echo b > b
-svn add b
-svn ci -m changea
-cd ..
-
-echo % convert to hg once
-hg convert $svnurl
-
-echo % update svn repository again
-cd t2
-echo c >> a
-echo c >> b
-svn ci -m changeb
-cd ..
-
-echo % test incremental conversion
-hg convert $svnurl
-
-echo % test filemap
-echo 'include b' > filemap
-hg convert --filemap filemap $svnurl fmap
-echo '[extensions]' >> $HGRCPATH
-echo 'hgext.graphlog =' >> $HGRCPATH
-hg glog -R fmap --template '#rev# #desc|firstline# files: #files#\n'
-
-########################################
-
-echo "# now tests that it works with trunk/branches/tags layout"
-echo
-echo % initial svn import
-mkdir projA
-cd projA
-mkdir trunk
-mkdir branches
-mkdir tags
-cd ..
-
-svnurl=file://$svnpath/svn-repo/projA
-svn import -m "init projA" projA $svnurl | fix_path
-
-
-echo % update svn repository
-svn co $svnurl/trunk A | fix_path
-cd A
-echo hello > letter.txt
-svn add letter.txt
-svn ci -m hello
-
-echo world >> letter.txt
-svn ci -m world
-
-svn copy -m "tag v0.1" $svnurl/trunk $svnurl/tags/v0.1
-
-echo 'nice day today!' >> letter.txt
-svn ci -m "nice day"
-cd ..
-
-echo % convert to hg once
-hg convert $svnurl A-hg
-
-echo % update svn repository again
-cd A
-echo "see second letter" >> letter.txt
-echo "nice to meet you" > letter2.txt
-svn add letter2.txt
-svn ci -m "second letter"
-
-svn copy -m "tag v0.2" $svnurl/trunk $svnurl/tags/v0.2
-
-echo "blah-blah-blah" >> letter2.txt
-svn ci -m "work in progress"
-cd ..
-
-echo % test incremental conversion
-hg convert $svnurl A-hg
-
-cd A-hg
-hg glog --template '#rev# #desc|firstline# files: #files#\n'
-hg tags -q
-cd ..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-sink	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,93 @@
+#!/bin/sh
+
+"$TESTDIR/hghave" svn svn-bindings || exit 80
+
+echo "[extensions]" >> $HGRCPATH
+echo "convert = " >> $HGRCPATH
+
+hg init a
+
+echo a > a/a
+mkdir -p a/d1/d2
+echo b > a/d1/d2/b
+echo % add
+hg --cwd a ci -d '0 0' -A -m 'add a file'
+
+echo a >> a/a
+echo % modify
+hg --cwd a ci -d '1 0' -m 'modify a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=2 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+cmp a/a a-hg-wc/a && echo same || echo different
+
+hg --cwd a mv a b
+echo % rename
+hg --cwd a ci -d '2 0' -m 'rename a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+
+hg --cwd a cp b c
+echo % copy
+hg --cwd a ci -d '3 0' -m 'copy a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+
+hg --cwd a rm b
+echo % remove
+hg --cwd a ci -d '4 0' -m 'remove a file'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+ls a a-hg-wc
+
+chmod +x a/c
+echo % executable
+hg --cwd a ci -d '5 0' -m 'make a file executable'
+hg --cwd a tip -q
+
+hg convert -d svn a
+(cd a-hg-wc; svn up; svn st -v; svn log --xml -v --limit=1 | sed 's,<date>.*,<date/>,')
+test -x a-hg-wc/c && echo executable || echo not executable
+
+echo % branchy history
+
+hg init b
+echo base > b/b
+hg --cwd b ci -d '0 0' -Ambase
+
+echo left-1 >> b/b
+echo left-1 > b/left-1
+hg --cwd b ci -d '1 0' -Amleft-1
+
+echo left-2 >> b/b
+echo left-2 > b/left-2
+hg --cwd b ci -d '2 0' -Amleft-2
+
+hg --cwd b up 0
+
+echo right-1 >> b/b
+echo right-1 > b/right-1
+hg --cwd b ci -d '3 0' -Amright-1
+
+echo right-2 >> b/b
+echo right-2 > b/right-2
+hg --cwd b ci -d '4 0' -Amright-2
+
+hg --cwd b up -C 2
+hg --cwd b merge
+hg --cwd b revert -r 2 b
+hg --cwd b ci -d '5 0' -m 'merge'
+
+hg convert -d svn b
+echo % expect 4 changes
+(cd b-hg-wc; svn up; svn st -v; svn log --xml -v | sed 's,<date>.*,<date/>,')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-sink.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,281 @@
+% add
+adding a
+adding d1/d2/b
+% modify
+1:e0e2b8a9156b
+assuming destination a-hg
+initializing svn repo 'a-hg'
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+1 add a file
+0 modify a file
+At revision 2.
+                2        2 test         .
+                2        2 test         a
+                2        1 test         d1
+                2        1 test         d1/d2
+                2        1 test         d1/d2/b
+<?xml version="1.0"?>
+<log>
+<logentry
+   revision="2">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="M">/a</path>
+</paths>
+<msg>modify a file</msg>
+</logentry>
+<logentry
+   revision="1">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="A">/a</path>
+<path
+   action="A">/d1</path>
+<path
+   action="A">/d1/d2</path>
+<path
+   action="A">/d1/d2/b</path>
+</paths>
+<msg>add a file</msg>
+</logentry>
+</log>
+a:
+a
+d1
+
+a-hg-wc:
+a
+d1
+same
+% rename
+2:7009fc4efb34
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 rename a file
+At revision 3.
+                3        3 test         .
+                3        3 test         b
+                3        1 test         d1
+                3        1 test         d1/d2
+                3        1 test         d1/d2/b
+<?xml version="1.0"?>
+<log>
+<logentry
+   revision="3">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="D">/a</path>
+<path
+   copyfrom-path="/a"
+   copyfrom-rev="2"
+   action="A">/b</path>
+</paths>
+<msg>rename a file</msg>
+</logentry>
+</log>
+a:
+b
+d1
+
+a-hg-wc:
+b
+d1
+% copy
+3:56c519973ce6
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 copy a file
+At revision 4.
+                4        4 test         .
+                4        3 test         b
+                4        4 test         c
+                4        1 test         d1
+                4        1 test         d1/d2
+                4        1 test         d1/d2/b
+<?xml version="1.0"?>
+<log>
+<logentry
+   revision="4">
+<author>test</author>
+<date/>
+<paths>
+<path
+   copyfrom-path="/b"
+   copyfrom-rev="3"
+   action="A">/c</path>
+</paths>
+<msg>copy a file</msg>
+</logentry>
+</log>
+a:
+b
+c
+d1
+
+a-hg-wc:
+b
+c
+d1
+% remove
+4:ed4dc9a6f585
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 remove a file
+At revision 5.
+                5        5 test         .
+                5        4 test         c
+                5        1 test         d1
+                5        1 test         d1/d2
+                5        1 test         d1/d2/b
+<?xml version="1.0"?>
+<log>
+<logentry
+   revision="5">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="D">/b</path>
+</paths>
+<msg>remove a file</msg>
+</logentry>
+</log>
+a:
+c
+d1
+
+a-hg-wc:
+c
+d1
+% executable
+5:f205b3636d77
+svn: Path 'b' does not exist
+assuming destination a-hg
+initializing svn wc 'a-hg-wc'
+scanning source...
+sorting...
+converting...
+0 make a file executable
+abort: svn exited with status 1
+At revision 5.
+                5        5 test         .
+ M              5        4 test         c
+                5        1 test         d1
+                5        1 test         d1/d2
+                5        1 test         d1/d2/b
+<?xml version="1.0"?>
+<log>
+<logentry
+   revision="5">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="D">/b</path>
+</paths>
+<msg>remove a file</msg>
+</logentry>
+</log>
+executable
+% branchy history
+adding b
+adding left-1
+adding left-2
+1 files updated, 0 files merged, 2 files removed, 0 files unresolved
+adding right-1
+adding right-2
+3 files updated, 0 files merged, 2 files removed, 0 files unresolved
+warning: conflicts during merge.
+merging b
+merging b failed!
+2 files updated, 0 files merged, 0 files removed, 1 files unresolved
+There are unresolved merges, you can redo the full merge using:
+  hg update -C 2
+  hg merge 4
+assuming destination b-hg
+initializing svn repo 'b-hg'
+initializing svn wc 'b-hg-wc'
+scanning source...
+sorting...
+converting...
+5 base
+4 left-1
+3 left-2
+2 right-1
+1 right-2
+0 merge
+% expect 4 changes
+At revision 4.
+                4        4 test         .
+                4        3 test         b
+                4        2 test         left-1
+                4        3 test         left-2
+                4        4 test         right-1
+                4        4 test         right-2
+<?xml version="1.0"?>
+<log>
+<logentry
+   revision="4">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="A">/right-1</path>
+<path
+   action="A">/right-2</path>
+</paths>
+<msg>merge</msg>
+</logentry>
+<logentry
+   revision="3">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="M">/b</path>
+<path
+   action="A">/left-2</path>
+</paths>
+<msg>left-2</msg>
+</logentry>
+<logentry
+   revision="2">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="M">/b</path>
+<path
+   action="A">/left-1</path>
+</paths>
+<msg>left-1</msg>
+</logentry>
+<logentry
+   revision="1">
+<author>test</author>
+<date/>
+<paths>
+<path
+   action="A">/b</path>
+</paths>
+<msg>base</msg>
+</logentry>
+</log>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-source	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,115 @@
+#!/bin/sh
+
+"$TESTDIR/hghave" svn svn-bindings || exit 80
+
+fix_path()
+{
+    tr '\\' /
+}
+
+echo "[extensions]" >> $HGRCPATH
+echo "convert = " >> $HGRCPATH
+
+svnadmin create svn-repo
+
+echo % initial svn import
+mkdir t
+cd t
+echo a > a
+cd ..
+
+svnpath=`pwd | fix_path`
+# SVN wants all paths to start with a slash. Unfortunately,
+# Windows ones don't. Handle that.
+expr $svnpath : "\/" > /dev/null
+if [ $? -ne 0 ]; then
+    svnpath='/'$svnpath
+fi
+
+svnurl=file://$svnpath/svn-repo/trunk
+svn import -m init t $svnurl | fix_path
+
+echo % update svn repository
+svn co $svnurl t2 | fix_path
+cd t2
+echo b >> a
+echo b > b
+svn add b
+svn ci -m changea
+cd ..
+
+echo % convert to hg once
+hg convert $svnurl
+
+echo % update svn repository again
+cd t2
+echo c >> a
+echo c >> b
+svn ci -m changeb
+cd ..
+
+echo % test incremental conversion
+hg convert $svnurl
+
+echo % test filemap
+echo 'include b' > filemap
+hg convert --filemap filemap $svnurl fmap
+echo '[extensions]' >> $HGRCPATH
+echo 'hgext.graphlog =' >> $HGRCPATH
+hg glog -R fmap --template '#rev# #desc|firstline# files: #files#\n'
+
+########################################
+
+echo "# now tests that it works with trunk/branches/tags layout"
+echo
+echo % initial svn import
+mkdir projA
+cd projA
+mkdir trunk
+mkdir branches
+mkdir tags
+cd ..
+
+svnurl=file://$svnpath/svn-repo/projA
+svn import -m "init projA" projA $svnurl | fix_path
+
+
+echo % update svn repository
+svn co $svnurl/trunk A | fix_path
+cd A
+echo hello > letter.txt
+svn add letter.txt
+svn ci -m hello
+
+echo world >> letter.txt
+svn ci -m world
+
+svn copy -m "tag v0.1" $svnurl/trunk $svnurl/tags/v0.1
+
+echo 'nice day today!' >> letter.txt
+svn ci -m "nice day"
+cd ..
+
+echo % convert to hg once
+hg convert $svnurl A-hg
+
+echo % update svn repository again
+cd A
+echo "see second letter" >> letter.txt
+echo "nice to meet you" > letter2.txt
+svn add letter2.txt
+svn ci -m "second letter"
+
+svn copy -m "tag v0.2" $svnurl/trunk $svnurl/tags/v0.2
+
+echo "blah-blah-blah" >> letter2.txt
+svn ci -m "work in progress"
+cd ..
+
+echo % test incremental conversion
+hg convert $svnurl A-hg
+
+cd A-hg
+hg glog --template '#rev# #desc|firstline# files: #files#\n'
+hg tags -q
+cd ..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-convert-svn-source.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,112 @@
+% initial svn import
+Adding         t/a
+
+Committed revision 1.
+% update svn repository
+A    t2/a
+Checked out revision 1.
+A         b
+Sending        a
+Adding         b
+Transmitting file data ..
+Committed revision 2.
+% convert to hg once
+assuming destination trunk-hg
+initializing destination trunk-hg repository
+scanning source...
+sorting...
+converting...
+1 init
+0 changea
+% update svn repository again
+Sending        a
+Sending        b
+Transmitting file data ..
+Committed revision 3.
+% test incremental conversion
+assuming destination trunk-hg
+scanning source...
+sorting...
+converting...
+0 changeb
+% test filemap
+initializing destination fmap repository
+scanning source...
+sorting...
+converting...
+2 init
+1 changea
+0 changeb
+o  1 changeb files: b
+|
+o  0 changea files: b
+
+# now tests that it works with trunk/branches/tags layout
+
+% initial svn import
+Adding         projA/trunk
+Adding         projA/branches
+Adding         projA/tags
+
+Committed revision 4.
+% update svn repository
+Checked out revision 4.
+A         letter.txt
+Adding         letter.txt
+Transmitting file data .
+Committed revision 5.
+Sending        letter.txt
+Transmitting file data .
+Committed revision 6.
+
+Committed revision 7.
+Sending        letter.txt
+Transmitting file data .
+Committed revision 8.
+% convert to hg once
+initializing destination A-hg repository
+scanning source...
+sorting...
+converting...
+3 init projA
+2 hello
+1 world
+0 nice day
+updating tags
+% update svn repository again
+A         letter2.txt
+Sending        letter.txt
+Adding         letter2.txt
+Transmitting file data ..
+Committed revision 9.
+
+Committed revision 10.
+Sending        letter2.txt
+Transmitting file data .
+Committed revision 11.
+% test incremental conversion
+scanning source...
+sorting...
+converting...
+1 second letter
+0 work in progress
+updating tags
+o  7 update tags files: .hgtags
+|
+o  6 work in progress files: letter2.txt
+|
+o  5 second letter files: letter.txt letter2.txt
+|
+o  4 update tags files: .hgtags
+|
+o  3 nice day files: letter.txt
+|
+o  2 world files: letter.txt
+|
+o  1 hello files: letter.txt
+|
+o  0 init projA files:
+
+tip
+v0.2
+v0.1
--- a/tests/test-convert-svn.out	Tue Dec 04 23:10:13 2007 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-% initial svn import
-Adding         t/a
-
-Committed revision 1.
-% update svn repository
-A    t2/a
-Checked out revision 1.
-A         b
-Sending        a
-Adding         b
-Transmitting file data ..
-Committed revision 2.
-% convert to hg once
-assuming destination trunk-hg
-initializing destination trunk-hg repository
-scanning source...
-sorting...
-converting...
-1 init
-0 changea
-% update svn repository again
-Sending        a
-Sending        b
-Transmitting file data ..
-Committed revision 3.
-% test incremental conversion
-assuming destination trunk-hg
-destination trunk-hg is a Mercurial repository
-scanning source...
-sorting...
-converting...
-0 changeb
-% test filemap
-initializing destination fmap repository
-scanning source...
-sorting...
-converting...
-2 init
-1 changea
-0 changeb
-o  1 changeb files: b
-|
-o  0 changea files: b
-
-# now tests that it works with trunk/branches/tags layout
-
-% initial svn import
-Adding         projA/trunk
-Adding         projA/branches
-Adding         projA/tags
-
-Committed revision 4.
-% update svn repository
-Checked out revision 4.
-A         letter.txt
-Adding         letter.txt
-Transmitting file data .
-Committed revision 5.
-Sending        letter.txt
-Transmitting file data .
-Committed revision 6.
-
-Committed revision 7.
-Sending        letter.txt
-Transmitting file data .
-Committed revision 8.
-% convert to hg once
-initializing destination A-hg repository
-scanning source...
-sorting...
-converting...
-3 init projA
-2 hello
-1 world
-0 nice day
-updating tags
-% update svn repository again
-A         letter2.txt
-Sending        letter.txt
-Adding         letter2.txt
-Transmitting file data ..
-Committed revision 9.
-
-Committed revision 10.
-Sending        letter2.txt
-Transmitting file data .
-Committed revision 11.
-% test incremental conversion
-destination A-hg is a Mercurial repository
-scanning source...
-sorting...
-converting...
-1 second letter
-0 work in progress
-updating tags
-o  7 update tags files: .hgtags
-|
-o  6 work in progress files: letter2.txt
-|
-o  5 second letter files: letter.txt letter2.txt
-|
-o  4 update tags files: .hgtags
-|
-o  3 nice day files: letter.txt
-|
-o  2 world files: letter.txt
-|
-o  1 hello files: letter.txt
-|
-o  0 init projA files:
-
-tip
-v0.2
-v0.1
--- a/tests/test-convert.out	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-convert.out	Tue Dec 04 23:28:10 2007 +0100
@@ -11,6 +11,7 @@
 
     Accepted destination formats:
     - Mercurial
+    - Subversion (history on branches is not preserved)
 
     If no revision is given, all revisions will be converted. Otherwise,
     convert will only import up to the named revision (given in a format
@@ -54,6 +55,24 @@
     subdirectory into the root of the repository, use '.' as the path to
     rename to.
 
+    Back end options:
+
+    --config convert.hg.clonebranches=False   (boolean)
+        hg target: XXX not documented
+    --config convert.hg.saverev=True          (boolean)
+        hg source: allow target to preserve source revision ID
+    --config convert.hg.tagsbranch=default    (branch name)
+        hg target: XXX not documented
+    --config convert.hg.usebranchnames=True   (boolean)
+        hg target: preserve branch names
+
+    --config convert.svn.branches=branches    (directory name)
+        svn source: specify the directory containing branches
+    --config convert.svn.tags=tags            (directory name)
+        svn source: specify the directory containing tags
+    --config convert.svn.trunk=trunk          (directory name)
+        svn source: specify the name of the trunk branch
+
 options:
 
  -A --authors      username mapping filename
--- a/tests/test-execute-bit	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-execute-bit	Tue Dec 04 23:28:10 2007 +0100
@@ -1,5 +1,7 @@
 #!/bin/sh
 
+"$TESTDIR/hghave" execbit || exit 80
+
 hg init
 echo a > a
 hg ci -d'0 0' -Am'not executable'
--- a/tests/test-hgweb	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-hgweb	Tue Dec 04 23:28:10 2007 +0100
@@ -1,4 +1,5 @@
 #!/bin/sh
+# Some tests for hgweb. Tests static files, plain files and different 404's.
 
 hg init test
 cd test
@@ -7,7 +8,25 @@
 echo foo > foo
 hg ci -Ambase -d '0 0'
 hg serve -p $HGPORT -d --pid-file=hg.pid
+cat hg.pid >> $DAEMON_PIDS
 echo % manifest
 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=raw')
 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/da?style=raw')
-kill `cat hg.pid`
+
+echo % plain file
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?style=raw'
+
+echo % should give a 404 - static file that does not exist
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/bogus'
+
+echo % should give a 404 - bad revision
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/spam/foo?style=raw'
+
+echo % should give a 400 - bad command
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?cmd=spam&style=raw' | sed 's/400.*/400/'
+
+echo % should give a 404 - file does not exist
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork?style=raw'
+
+echo % static file
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/style-gitweb.css'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgweb-commands	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,53 @@
+#!/bin/sh
+# An attempt at more fully testing the hgweb web interface.
+# The following things are tested elsewhere and are therefore omitted:
+# - archive, tested in test-archive
+# - unbundle, tested in test-push-http
+# - changegroupsubset, tested in test-pull
+
+echo % Set up the repo
+hg init test
+cd test
+mkdir da
+echo foo > da/foo
+echo foo > foo
+hg ci -d'0 0' -Ambase
+hg tag 1.0
+hg serve -n test -p $HGPORT -d --pid-file=hg.pid -E errors.log
+cat hg.pid >> $DAEMON_PIDS
+
+echo % Logs and changes
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/log/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/log/1/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/log/1/foo/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/shortlog/' | sed "s/[0-9]* years/many years/"
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/rev/1/?style=raw'
+
+echo % File-related
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/file/1/foo/?style=raw'
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/annotate/1/foo/?style=raw'
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/file/1/?style=raw'
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/filediff/1/foo/?style=raw'
+
+echo % Overviews
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/tags/?style=atom' | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/summary/?style=gitweb' | sed "s/[0-9]* years ago/long ago/"
+
+echo % capabilities
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/capabilities'
+echo % heads
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/heads'
+echo % lookup
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/lookup/1'
+echo % branches
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/branches'
+echo % changegroup
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/changegroup'
+echo % stream_out
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/stream_out'
+
+echo % Static files
+"$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '/static/style.css'
+
+echo % ERRORS ENCOUNTERED
+cat errors.log
Binary file tests/test-hgweb-commands.out has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgweb-no-request-uri	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,77 @@
+#!/bin/sh
+# This tests if hgweb and hgwebdir still work if the REQUEST_URI variable is
+# no longer passed with the request. Instead, SCRIPT_NAME and PATH_INFO
+# should be used from d74fc8dec2b4 onward to route the request.
+
+mkdir repo
+cd repo
+hg init
+echo foo > bar
+hg add bar
+hg commit -m "test" -d "0 0" -u "Testing"
+hg tip
+
+cat > request.py <<EOF
+from mercurial.hgweb import hgweb, hgwebdir
+from StringIO import StringIO
+import os, sys
+
+errors = StringIO()
+input = StringIO()
+
+def startrsp(headers, data):
+	print '---- HEADERS'
+	print headers
+	print '---- DATA'
+	print data
+	return output.write
+
+env = {
+	'wsgi.version': (1, 0),
+	'wsgi.url_scheme': 'http',
+	'wsgi.errors': errors,
+	'wsgi.input': input,
+	'wsgi.multithread': False,
+	'wsgi.multiprocess': False,
+	'wsgi.run_once': False,
+	'REQUEST_METHOD': 'GET',
+	'SCRIPT_NAME': '',
+	'SERVER_NAME': '127.0.0.1',
+	'SERVER_PORT': os.environ['HGPORT'],
+	'SERVER_PROTOCOL': 'HTTP/1.0'
+}
+
+output = StringIO()
+env['PATH_INFO'] = '/'
+env['QUERY_STRING'] = 'style=atom'
+hgweb('.', name = 'repo')(env, startrsp)
+print output.getvalue()
+print '---- ERRORS'
+print errors.getvalue()
+
+output = StringIO()
+env['PATH_INFO'] = '/file/tip/'
+env['QUERY_STRING'] = 'style=raw'
+hgweb('.', name = 'repo')(env, startrsp)
+print output.getvalue()
+print '---- ERRORS'
+print errors.getvalue()
+
+output = StringIO()
+env['PATH_INFO'] = '/'
+env['QUERY_STRING'] = 'style=raw'
+hgwebdir({'repo': '.'})(env, startrsp)
+print output.getvalue()
+print '---- ERRORS'
+print errors.getvalue()
+
+output = StringIO()
+env['PATH_INFO'] = '/repo/file/tip/'
+env['QUERY_STRING'] = 'style=raw'
+hgwebdir({'repo': '.'})(env, startrsp)
+print output.getvalue()
+print '---- ERRORS'
+print errors.getvalue()
+EOF
+
+python request.py | sed "s/http:\/\/127\.0\.0\.1:[0-9]*\//http:\/\/127.0.0.1\//"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgweb-no-request-uri.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,72 @@
+changeset:   0:4cbec7e6f8c4
+tag:         tip
+user:        Testing
+date:        Thu Jan 01 00:00:00 1970 +0000
+summary:     test
+
+---- HEADERS
+200 Script output follows
+---- DATA
+[('content-type', 'application/atom+xml; charset=ascii')]
+<?xml version="1.0" encoding="ascii"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <!-- Changelog -->
+ <id>http://127.0.0.1/</id>
+ <link rel="self" href="http://127.0.0.1/atom-log"/>
+ <link rel="alternate" href="http://127.0.0.1/"/>
+ <title>repo Changelog</title>
+ <updated>1970-01-01T00:00:00+00:00</updated>
+
+ <entry>
+  <title>test</title>
+  <id>http://www.selenic.com/mercurial/#changeset-4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e</id>
+  <link href="http://127.0.0.1/rev/4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e"/>
+  <author>
+   <name>Testing</name>
+   <email>&#84;&#101;&#115;&#116;&#105;&#110;&#103;</email>
+  </author>
+  <updated>1970-01-01T00:00:00+00:00</updated>
+  <published>1970-01-01T00:00:00+00:00</published>
+  <content type="xhtml">
+   <div xmlns="http://www.w3.org/1999/xhtml">
+    <pre xml:space="preserve">test</pre>
+   </div>
+  </content>
+ </entry>
+
+</feed>
+
+---- ERRORS
+
+---- HEADERS
+200 Script output follows
+---- DATA
+[('content-type', 'text/plain; charset=ascii')]
+
+-rw-r--r-- 4 bar
+
+
+
+---- ERRORS
+
+---- HEADERS
+200 Script output follows
+---- DATA
+[('content-type', 'text/plain; charset=ascii')]
+
+/repo/
+
+
+---- ERRORS
+
+---- HEADERS
+200 Script output follows
+---- DATA
+[('content-type', 'text/plain; charset=ascii')]
+
+-rw-r--r-- 4 bar
+
+
+
+---- ERRORS
+
--- a/tests/test-hgweb.out	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-hgweb.out	Tue Dec 04 23:28:10 2007 +0100
@@ -14,3 +14,123 @@
 -rw-r--r-- 4 foo
 
 
+% plain file
+200 Script output follows
+
+foo
+% should give a 404 - static file that does not exist
+404 Not Found
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<link rel="icon" href="/static/hgicon.png" type="image/png">
+<meta name="robots" content="index, nofollow" />
+<link rel="stylesheet" href="/static/style.css" type="text/css" />
+
+<title>Mercurial Error</title>
+</head>
+<body>
+
+<h2>Mercurial Error</h2>
+
+<p>
+An error occured while processing your request:
+</p>
+<p>
+Not Found
+</p>
+
+
+<div class="logo">
+powered by<br/>
+<a href="http://www.selenic.com/mercurial/">mercurial</a>
+</div>
+
+</body>
+</html>
+
+% should give a 404 - bad revision
+404 Not Found
+
+
+error: revision not found: spam
+% should give a 400 - bad command
+400
+
+
+error: No such method: spam
+% should give a 404 - file does not exist
+404 Not Found
+
+
+error: Path not found: bork/
+% static file
+200 Script output follows
+
+body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; }
+a { color:#0000cc; }
+a:hover, a:visited, a:active { color:#880000; }
+div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
+div.page_header a:visited { color:#0000cc; }
+div.page_header a:hover { color:#880000; }
+div.page_nav { padding:8px; }
+div.page_nav a:visited { color:#0000cc; }
+div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
+div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
+div.page_footer_text { float:left; color:#555555; font-style:italic; }
+div.page_body { padding:8px; }
+div.title, a.title {
+	display:block; padding:6px 8px;
+	font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
+}
+a.title:hover { background-color: #d9d8d1; }
+div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
+div.log_body { padding:8px 8px 8px 150px; }
+.age { white-space:nowrap; }
+span.age { position:relative; float:left; width:142px; font-style:italic; }
+div.log_link {
+	padding:0px 8px;
+	font-size:10px; font-family:sans-serif; font-style:normal;
+	position:relative; float:left; width:136px;
+}
+div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
+a.list { text-decoration:none; color:#000000; }
+a.list:hover { text-decoration:underline; color:#880000; }
+table { padding:8px 4px; }
+th { padding:2px 5px; font-size:12px; text-align:left; }
+tr.light:hover, .parity0:hover { background-color:#edece6; }
+tr.dark, .parity1 { background-color:#f6f6f0; }
+tr.dark:hover, .parity1:hover { background-color:#edece6; }
+td { padding:2px 5px; font-size:12px; vertical-align:top; }
+td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
+div.pre { font-family:monospace; font-size:12px; white-space:pre; }
+div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
+div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
+div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
+.linenr { color:#999999; text-decoration:none }
+a.rss_logo {
+	float:right; padding:3px 0px; width:35px; line-height:10px;
+	border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
+	color:#ffffff; background-color:#ff6600;
+	font-weight:bold; font-family:sans-serif; font-size:10px;
+	text-align:center; text-decoration:none;
+}
+a.rss_logo:hover { background-color:#ee5500; }
+pre { margin: 0; }
+span.logtags span {
+	padding: 0px 4px;
+	font-size: 10px;
+	font-weight: normal;
+	border: 1px solid;
+	background-color: #ffaaff;
+	border-color: #ffccff #ff00ee #ff00ee #ffccff;
+}
+span.logtags span.tagtag {
+	background-color: #ffffaa;
+	border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
+}
+span.logtags span.branchtag {
+	background-color: #aaffaa;
+	border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgwebdir	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,84 @@
+#!/bin/sh
+# Tests some basic hgwebdir functionality. Tests setting up paths and
+# collection, different forms of 404s and the subdirectory support.
+
+mkdir webdir
+cd webdir
+
+hg init a
+echo a > a/a
+hg --cwd a ci -Ama -d'1 0'
+
+hg init b
+echo b > b/b
+hg --cwd b ci -Amb -d'2 0'
+
+hg init c
+echo c > c/c
+hg --cwd c ci -Amc -d'3 0'
+root=`pwd`
+
+cd ..
+
+cat > paths.conf <<EOF
+[paths]
+a=$root/a
+b=$root/b
+EOF
+
+hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
+    -A access-paths.log -E error-paths-1.log
+cat hg.pid >> $DAEMON_PIDS
+
+echo % should give a 404 - file does not exist
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
+
+echo % should succeed
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
+
+echo % should give a 404 - repo is not published
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
+
+cat > paths.conf <<EOF
+[paths]
+t/a/=$root/a
+b=$root/b
+EOF
+
+hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
+    -A access-paths.log -E error-paths-2.log
+cat hg.pid >> $DAEMON_PIDS
+
+echo % should succeed, slashy names
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom' \
+	| sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom' \
+	| sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw'
+
+cat > collections.conf <<EOF
+[collections]
+$root=$root
+EOF
+
+hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections.conf \
+    -A access-collections.log -E error-collections.log
+cat hg.pid >> $DAEMON_PIDS
+
+echo % should succeed
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw'
+"$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw'
+
+echo % paths errors 1
+cat error-paths-1.log
+echo % paths errors 2
+cat error-paths-2.log
+echo % collections errors
+cat error-collections.log
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-hgwebdir.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,124 @@
+adding a
+adding b
+adding c
+% should give a 404 - file does not exist
+404 Not Found
+
+
+error: Path not found: bork/
+% should succeed
+200 Script output follows
+
+
+/a/
+/b/
+
+200 Script output follows
+
+a
+200 Script output follows
+
+b
+% should give a 404 - repo is not published
+404 Not Found
+
+
+error: repository c not found
+% should succeed, slashy names
+200 Script output follows
+
+
+/b/
+/t/a/
+
+200 Script output follows
+
+
+/t/a/
+
+200 Script output follows
+
+
+/t/a/
+
+200 Script output follows
+
+<?xml version="1.0" encoding="ascii"?>
+<feed xmlns="http://127.0.0.1/2005/Atom">
+ <!-- Changelog -->
+ <id>http://127.0.0.1/t/a/</id>
+ <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
+ <link rel="alternate" href="http://127.0.0.1/t/a/"/>
+ <title>t/a Changelog</title>
+ <updated>1970-01-01T00:00:01+00:00</updated>
+
+ <entry>
+  <title>a</title>
+  <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
+  <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
+  <author>
+   <name>test</name>
+   <email>&#116;&#101;&#115;&#116;</email>
+  </author>
+  <updated>1970-01-01T00:00:01+00:00</updated>
+  <published>1970-01-01T00:00:01+00:00</published>
+  <content type="xhtml">
+   <div xmlns="http://127.0.0.1/1999/xhtml">
+    <pre xml:space="preserve">a</pre>
+   </div>
+  </content>
+ </entry>
+
+</feed>
+200 Script output follows
+
+<?xml version="1.0" encoding="ascii"?>
+<feed xmlns="http://127.0.0.1/2005/Atom">
+ <!-- Changelog -->
+ <id>http://127.0.0.1/t/a/</id>
+ <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
+ <link rel="alternate" href="http://127.0.0.1/t/a/"/>
+ <title>t/a Changelog</title>
+ <updated>1970-01-01T00:00:01+00:00</updated>
+
+ <entry>
+  <title>a</title>
+  <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
+  <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
+  <author>
+   <name>test</name>
+   <email>&#116;&#101;&#115;&#116;</email>
+  </author>
+  <updated>1970-01-01T00:00:01+00:00</updated>
+  <published>1970-01-01T00:00:01+00:00</published>
+  <content type="xhtml">
+   <div xmlns="http://127.0.0.1/1999/xhtml">
+    <pre xml:space="preserve">a</pre>
+   </div>
+  </content>
+ </entry>
+
+</feed>
+200 Script output follows
+
+a
+% should succeed
+200 Script output follows
+
+
+/a/
+/b/
+/c/
+
+200 Script output follows
+
+a
+200 Script output follows
+
+b
+200 Script output follows
+
+c
+% paths errors 1
+% paths errors 2
+% collections errors
--- a/tests/test-import	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-import	Tue Dec 04 23:28:10 2007 +0100
@@ -57,7 +57,7 @@
 cat > mkmsg.py <<EOF
 import email.Message, sys
 msg = email.Message.Message()
-msg.set_payload('email commit message\n' + open('tip.patch').read())
+msg.set_payload('email commit message\n' + open('tip.patch', 'rb').read())
 msg['Subject'] = 'email patch'
 msg['From'] = 'email patcher'
 sys.stdout.write(msg.as_string())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-merge-types	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+hg init
+echo a > a
+hg ci -Amadd
+
+chmod +x a
+hg ci -mexecutable
+
+hg up 0
+rm a
+ln -s symlink a
+hg ci -msymlink
+
+hg merge
+
+echo % symlink is left parent, executable is right
+
+if [ -L a ]; then
+    echo a is a symlink
+    readlink a
+elif [ -x a ]; then
+    echo a is executable
+fi
+
+hg update -C 1
+hg merge
+
+echo % symlink is right parent, executable is left
+
+if [ -L a ]; then
+    echo a is a symlink
+    readlink a
+elif [ -x a ]; then
+    echo a is executable
+fi
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-merge-types.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,1 @@
+### This test is for a known, unfixed bug ###
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-newcgi	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,91 @@
+#!/bin/sh
+# This tests if CGI files from after d0db3462d568 but
+# before d74fc8dec2b4 still work.
+
+hg init test
+
+cat >hgweb.cgi <<HGWEB
+#!/usr/bin/env python
+#
+# An example CGI script to use hgweb, edit as necessary
+
+import cgitb
+cgitb.enable()
+
+from mercurial import demandimport; demandimport.enable()
+from mercurial.hgweb import hgweb
+from mercurial.hgweb import wsgicgi
+from mercurial.hgweb.request import wsgiapplication
+
+def make_web_app():
+	return hgweb("test", "Empty test repository")
+
+wsgicgi.launch(wsgiapplication(make_web_app))
+HGWEB
+chmod 755 hgweb.cgi
+
+cat >hgweb.config <<HGWEBDIRCONF
+[paths]
+test = test
+HGWEBDIRCONF
+
+cat >hgwebdir.cgi <<HGWEBDIR
+#!/usr/bin/env python
+#
+# An example CGI script to export multiple hgweb repos, edit as necessary
+
+import cgitb
+cgitb.enable()
+
+from mercurial import demandimport; demandimport.enable()
+from mercurial.hgweb import hgwebdir
+from mercurial.hgweb import wsgicgi
+from mercurial.hgweb.request import wsgiapplication
+
+def make_web_app():
+	return hgwebdir("hgweb.config")
+
+wsgicgi.launch(wsgiapplication(make_web_app))
+HGWEBDIR
+chmod 755 hgwebdir.cgi
+
+DOCUMENT_ROOT="/var/www/hg"; export DOCUMENT_ROOT
+GATEWAY_INTERFACE="CGI/1.1"; export GATEWAY_INTERFACE
+HTTP_ACCEPT="text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; export HTTP_ACCEPT
+HTTP_ACCEPT_CHARSET="ISO-8859-1,utf-8;q=0.7,*;q=0.7"; export HTTP_ACCEPT_CHARSET
+HTTP_ACCEPT_ENCODING="gzip,deflate"; export HTTP_ACCEPT_ENCODING
+HTTP_ACCEPT_LANGUAGE="en-us,en;q=0.5"; export HTTP_ACCEPT_LANGUAGE
+HTTP_CACHE_CONTROL="max-age=0"; export HTTP_CACHE_CONTROL
+HTTP_CONNECTION="keep-alive"; export HTTP_CONNECTION
+HTTP_HOST="hg.omnifarious.org"; export HTTP_HOST
+HTTP_KEEP_ALIVE="300"; export HTTP_KEEP_ALIVE
+HTTP_USER_AGENT="Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4"; export HTTP_USER_AGENT
+PATH_INFO="/"; export PATH_INFO
+PATH_TRANSLATED="/var/www/hg/index.html"; export PATH_TRANSLATED
+QUERY_STRING=""; export QUERY_STRING
+REMOTE_ADDR="127.0.0.2"; export REMOTE_ADDR
+REMOTE_PORT="44703"; export REMOTE_PORT
+REQUEST_METHOD="GET"; export REQUEST_METHOD
+REQUEST_URI="/test/"; export REQUEST_URI
+SCRIPT_FILENAME="/home/hopper/hg_public/test.cgi"; export SCRIPT_FILENAME
+SCRIPT_NAME="/test"; export SCRIPT_NAME
+SCRIPT_URI="http://hg.omnifarious.org/test/"; export SCRIPT_URI
+SCRIPT_URL="/test/"; export SCRIPT_URL
+SERVER_ADDR="127.0.0.1"; export SERVER_ADDR
+SERVER_ADMIN="eric@localhost"; export SERVER_ADMIN
+SERVER_NAME="hg.omnifarious.org"; export SERVER_NAME
+SERVER_PORT="80"; export SERVER_PORT
+SERVER_PROTOCOL="HTTP/1.1"; export SERVER_PROTOCOL
+SERVER_SIGNATURE="<address>Apache/2.0.53 (Fedora) Server at hg.omnifarious.org Port 80</address>\; export SERVER_SIGNATURE
+"
+SERVER_SOFTWARE="Apache/2.0.53 (Fedora)"; export SERVER_SOFTWARE
+python hgweb.cgi >page1 2>&1 ; echo $?
+python hgwebdir.cgi >page2 2>&1 ; echo $?
+PATH_INFO="/test/"
+PATH_TRANSLATED="/var/something/test.cgi"
+REQUEST_URI="/test/test/"
+SCRIPT_URI="http://hg.omnifarious.org/test/test/"
+SCRIPT_URL="/test/test/"
+python hgwebdir.cgi >page3 2>&1 ; echo $?
+fgrep -i error page1 page2 page3 && exit 1
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-newcgi.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,3 @@
+0
+0
+0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-newercgi	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,84 @@
+#!/bin/sh
+# This is a rudimentary test of the CGI files as of d74fc8dec2b4.
+
+hg init test
+
+cat >hgweb.cgi <<HGWEB
+#!/usr/bin/env python
+#
+# An example CGI script to use hgweb, edit as necessary
+
+import cgitb
+cgitb.enable()
+
+from mercurial import demandimport; demandimport.enable()
+from mercurial.hgweb import hgweb
+from mercurial.hgweb import wsgicgi
+
+application = hgweb("test", "Empty test repository")
+wsgicgi.launch(application)
+HGWEB
+chmod 755 hgweb.cgi
+
+cat >hgweb.config <<HGWEBDIRCONF
+[paths]
+test = test
+HGWEBDIRCONF
+
+cat >hgwebdir.cgi <<HGWEBDIR
+#!/usr/bin/env python
+#
+# An example CGI script to export multiple hgweb repos, edit as necessary
+
+import cgitb
+cgitb.enable()
+
+from mercurial import demandimport; demandimport.enable()
+from mercurial.hgweb import hgwebdir
+from mercurial.hgweb import wsgicgi
+
+application = hgwebdir("hgweb.config")
+wsgicgi.launch(application)
+HGWEBDIR
+chmod 755 hgwebdir.cgi
+
+DOCUMENT_ROOT="/var/www/hg"; export DOCUMENT_ROOT
+GATEWAY_INTERFACE="CGI/1.1"; export GATEWAY_INTERFACE
+HTTP_ACCEPT="text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; export HTTP_ACCEPT
+HTTP_ACCEPT_CHARSET="ISO-8859-1,utf-8;q=0.7,*;q=0.7"; export HTTP_ACCEPT_CHARSET
+HTTP_ACCEPT_ENCODING="gzip,deflate"; export HTTP_ACCEPT_ENCODING
+HTTP_ACCEPT_LANGUAGE="en-us,en;q=0.5"; export HTTP_ACCEPT_LANGUAGE
+HTTP_CACHE_CONTROL="max-age=0"; export HTTP_CACHE_CONTROL
+HTTP_CONNECTION="keep-alive"; export HTTP_CONNECTION
+HTTP_HOST="hg.omnifarious.org"; export HTTP_HOST
+HTTP_KEEP_ALIVE="300"; export HTTP_KEEP_ALIVE
+HTTP_USER_AGENT="Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4"; export HTTP_USER_AGENT
+PATH_INFO="/"; export PATH_INFO
+PATH_TRANSLATED="/var/www/hg/index.html"; export PATH_TRANSLATED
+QUERY_STRING=""; export QUERY_STRING
+REMOTE_ADDR="127.0.0.2"; export REMOTE_ADDR
+REMOTE_PORT="44703"; export REMOTE_PORT
+REQUEST_METHOD="GET"; export REQUEST_METHOD
+REQUEST_URI="/test/"; export REQUEST_URI
+SCRIPT_FILENAME="/home/hopper/hg_public/test.cgi"; export SCRIPT_FILENAME
+SCRIPT_NAME="/test"; export SCRIPT_NAME
+SCRIPT_URI="http://hg.omnifarious.org/test/"; export SCRIPT_URI
+SCRIPT_URL="/test/"; export SCRIPT_URL
+SERVER_ADDR="127.0.0.1"; export SERVER_ADDR
+SERVER_ADMIN="eric@localhost"; export SERVER_ADMIN
+SERVER_NAME="hg.omnifarious.org"; export SERVER_NAME
+SERVER_PORT="80"; export SERVER_PORT
+SERVER_PROTOCOL="HTTP/1.1"; export SERVER_PROTOCOL
+SERVER_SIGNATURE="<address>Apache/2.0.53 (Fedora) Server at hg.omnifarious.org Port 80</address>\; export SERVER_SIGNATURE
+"
+SERVER_SOFTWARE="Apache/2.0.53 (Fedora)"; export SERVER_SOFTWARE
+python hgweb.cgi >page1 2>&1 ; echo $?
+python hgwebdir.cgi >page2 2>&1 ; echo $?
+PATH_INFO="/test/"
+PATH_TRANSLATED="/var/something/test.cgi"
+REQUEST_URI="/test/test/"
+SCRIPT_URI="http://hg.omnifarious.org/test/test/"
+SCRIPT_URL="/test/test/"
+python hgwebdir.cgi >page3 2>&1 ; echo $?
+fgrep -i error page1 page2 page3 && exit 1
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-newercgi.out	Tue Dec 04 23:28:10 2007 +0100
@@ -0,0 +1,3 @@
+0
+0
+0
--- a/tests/test-non-interactive-wsgi	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-non-interactive-wsgi	Tue Dec 04 23:28:10 2007 +0100
@@ -1,4 +1,6 @@
 #!/bin/sh
+# Tests if hgweb can run without touching sys.stdin, as is required
+# by the WSGI standard and strictly implemented by mod_wsgi.
 
 mkdir repo
 cd repo
@@ -11,7 +13,6 @@
 cat > request.py <<EOF
 from mercurial import dispatch
 from mercurial.hgweb.hgweb_mod import hgweb
-from mercurial.hgweb.request import _wsgirequest
 from mercurial.ui import ui
 from mercurial import hg
 from StringIO import StringIO
@@ -62,7 +63,7 @@
 	'SERVER_PROTOCOL': 'HTTP/1.0'
 }
 
-_wsgirequest(hgweb('.'), env, startrsp)
+hgweb('.')(env, startrsp)
 print '---- ERRORS'
 print errors.getvalue()
 EOF
--- a/tests/test-oldcgi	Tue Dec 04 23:10:13 2007 +0100
+++ b/tests/test-oldcgi	Tue Dec 04 23:28:10 2007 +0100
@@ -1,4 +1,5 @@
 #!/bin/sh
+# This tests if CGI files from before d0db3462d568 still work.
 
 hg init test