changeset 4965:4106dde15aed

Merge with crew
author Matt Mackall <mpm@selenic.com>
date Sat, 21 Jul 2007 16:44:38 -0500
parents ee983d0dbea8 (current diff) a28661788f2f (diff)
children cf67b5f3743d
files hgext/convert/hg.py mercurial/cmdutil.py mercurial/commands.py mercurial/dirstate.py mercurial/patch.py
diffstat 21 files changed, 308 insertions(+), 168 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/churn.py
+++ b/contrib/churn.py
@@ -11,9 +11,34 @@
 #
 # <alias email> <actual email>
 
-import sys
 from mercurial.i18n import gettext as _
 from mercurial import hg, mdiff, cmdutil, ui, util, templater, node
+import os, sys
+
+def get_tty_width():
+    if 'COLUMNS' in os.environ:
+        try:
+            return int(os.environ['COLUMNS'])
+        except ValueError:
+            pass
+    try:
+        import termios, fcntl, struct
+        buf = 'abcd'
+        for dev in (sys.stdout, sys.stdin):
+            try:
+                if buf != 'abcd':
+                    break
+                fd = dev.fileno()
+                if not os.isatty(fd):
+                    continue
+                buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
+            except ValueError:
+                pass
+        if buf != 'abcd':
+           return struct.unpack('hh', buf)[1]
+    except ImportError:
+        pass
+    return 80
 
 def __gather(ui, repo, node1, node2):
     def dirtywork(f, mmap1, mmap2):
@@ -159,8 +184,9 @@ def churn(ui, repo, **opts):
 
     maximum = ordered[0][1]
 
-    ui.note("Assuming 80 character terminal\n")
-    width = 80 - 1
+    width = get_tty_width()
+    ui.note(_("assuming %i character terminal\n") % width)
+    width -= 1
 
     for i in ordered:
         person = i[0]
--- a/hgext/alias.py
+++ b/hgext/alias.py
@@ -72,6 +72,5 @@ def uisetup(ui):
         args = target.split(' ')
         tcmd = args.pop(0)
         if args:
-            pui = ui.parentui or ui
-            pui.setconfig('defaults', cmd, ' '.join(args))
+            ui.setconfig('defaults', cmd, ' '.join(args))
         cmdtable[cmd] = lazycommand(ui, cmd, tcmd)
--- a/hgext/convert/__init__.py
+++ b/hgext/convert/__init__.py
@@ -192,7 +192,7 @@ class convert(object):
     def copy(self, rev):
         c = self.commitcache[rev]
         files = self.source.getchanges(rev)
-        
+
         do_copies = (hasattr(c, 'copies') and hasattr(self.dest, 'copyfile'))
 
         for f, v in files:
@@ -260,7 +260,7 @@ class convert(object):
            self.mapfilefd.close()
 
 def _convert(ui, src, dest=None, mapfile=None, **opts):
-    '''Convert a foreign SCM repository to a Mercurial one.
+    """Convert a foreign SCM repository to a Mercurial one.
 
     Accepted source formats:
     - GIT
@@ -293,7 +293,7 @@ def _convert(ui, src, dest=None, mapfile
     that use unix logins to identify authors (eg: CVS). One line per author
     mapping and the line format is:
     srcauthor=whatever string you want
-    '''
+    """
 
     util._encoding = 'UTF-8'
 
--- a/hgext/convert/common.py
+++ b/hgext/convert/common.py
@@ -60,7 +60,7 @@ class converter_source(object):
     def recode(self, s, encoding=None):
         if not encoding:
             encoding = self.encoding or 'utf-8'
-            
+
         try:
             return s.decode(encoding).encode("utf-8")
         except:
--- a/hgext/convert/subversion.py
+++ b/hgext/convert/subversion.py
@@ -1,10 +1,21 @@
 # Subversion 1.4/1.5 Python API backend
 #
 # Copyright(C) 2007 Daniel Holth et al
+#
+# Configuration options:
+#
+# convert.svn.trunk
+#   Relative path to the trunk (default: "trunk")
+# convert.svn.branches
+#   Relative path to tree of branches (default: "branches")
+#
+# Set these in a hgrc, or on the command line as follows:
+#
+#   hg convert --config convert.svn.trunk=wackoname [...]
 
-import pprint
 import locale
-
+import os
+import cPickle as pickle
 from mercurial import util
 
 # Subversion stuff. Works best with very recent Python SVN bindings
@@ -27,6 +38,12 @@ except ImportError:
 
 class CompatibilityException(Exception): pass
 
+class changedpath(object):
+    def __init__(self, p):
+        self.copyfrom_path = p.copyfrom_path
+        self.copyfrom_rev = p.copyfrom_rev
+        self.action = p.action
+
 # SVN conversion code stolen from bzr-svn and tailor
 class convert_svn(converter_source):
     def __init__(self, ui, url, rev=None):
@@ -51,16 +68,18 @@ class convert_svn(converter_source):
         try:
             # Support file://path@rev syntax. Useful e.g. to convert
             # deleted branches.
-            url, latest = url.rsplit("@", 1)
-            latest = int(latest)
+            at = url.rfind('@')
+            if at >= 0:
+                latest = int(url[at+1:])
+                url = url[:at]
         except ValueError, e:
             pass
         self.url = url
         self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
         try:
-            self.transport = transport.SvnRaTransport(url = url)
+            self.transport = transport.SvnRaTransport(url=url)
             self.ra = self.transport.ra
-            self.ctx = svn.client.create_context()
+            self.ctx = self.transport.client
             self.base = svn.ra.get_repos_root(self.ra)
             self.module = self.url[len(self.base):]
             self.modulemap = {} # revision, module
@@ -88,26 +107,47 @@ class convert_svn(converter_source):
                 lastrevs[module] = revnum
         self.lastrevs = lastrevs
 
+    def exists(self, path, optrev):
+        try:
+            return svn.client.ls(self.url.rstrip('/') + '/' + path,
+                                 optrev, False, self.ctx)
+        except SubversionException, err:
+            return []
+
     def getheads(self):
         # detect standard /branches, /tags, /trunk layout
         optrev = svn.core.svn_opt_revision_t()
         optrev.kind = svn.core.svn_opt_revision_number
         optrev.value.number = self.last_changed
         rpath = self.url.strip('/')
-        paths = svn.client.ls(rpath, optrev, False, self.ctx)
-        if 'branches' in paths and 'trunk' in paths:
-            self.module += '/trunk'
+        cfgtrunk = self.ui.config('convert', 'svn.trunk')
+        cfgbranches = self.ui.config('convert', 'svn.branches')
+        trunk = (cfgtrunk or 'trunk').strip('/')
+        branches = (cfgbranches or 'branches').strip('/')
+        if self.exists(trunk, optrev) and self.exists(branches, optrev):
+            self.ui.note('found trunk at %r and branches at %r\n' %
+                         (trunk, branches))
+            oldmodule = self.module
+            self.module += '/' + trunk
             lt = self.latest(self.module, self.last_changed)
             self.head = self.revid(lt)
             self.heads = [self.head]
-            branches = svn.client.ls(rpath + '/branches', optrev, False, self.ctx)
-            for branch in branches.keys():
-                module = '/branches/' + branch
+            branchnames = svn.client.ls(rpath + '/' + branches, optrev, False,
+                                        self.ctx)
+            for branch in branchnames.keys():
+                if oldmodule:
+                    module = '/' + oldmodule + '/' + branches + '/' + branch
+                else:
+                    module = '/' + branches + '/' + branch
                 brevnum = self.latest(module, self.last_changed)
                 brev = self.revid(brevnum, module)
                 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
                 self.heads.append(brev)
+        elif cfgtrunk or cfgbranches:
+            raise util.Abort(_('trunk/branch layout expected, '
+                               'but not found'))
         else:
+            self.ui.note('working with one branch\n')
             self.heads = [self.head]
         return self.heads
 
@@ -116,7 +156,7 @@ class convert_svn(converter_source):
         self.modecache[(file, rev)] = mode
         return data
 
-    def getmode(self, file, rev):        
+    def getmode(self, file, rev):
         return self.modecache[(file, rev)]
 
     def getchanges(self, rev):
@@ -140,27 +180,79 @@ class convert_svn(converter_source):
         del self.commits[rev]
         return commit
 
+    def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
+                strict_node_history=False):
+        '''wrapper for svn.ra.get_log.
+        on a large repository, svn.ra.get_log pins huge amounts of
+        memory that cannot be recovered.  work around it by forking
+        and writing results over a pipe.'''
+
+        def child(fp):
+            protocol = -1
+            def receiver(orig_paths, revnum, author, date, message, pool):
+                if orig_paths is not None:
+                    for k, v in orig_paths.iteritems():
+                        orig_paths[k] = changedpath(v)
+                pickle.dump((orig_paths, revnum, author, date, message),
+                            fp, protocol)
+
+            try:
+                # Use an ra of our own so that our parent can consume
+                # our results without confusing the server.
+                t = transport.SvnRaTransport(url=self.url)
+                svn.ra.get_log(t.ra, paths, start, end, limit,
+                               discover_changed_paths,
+                               strict_node_history,
+                               receiver)
+            except SubversionException, (_, num):
+                self.ui.print_exc()
+                pickle.dump(num, fp, protocol)
+            else:
+                pickle.dump(None, fp, protocol)
+            fp.close()
+
+        def parent(fp):
+            while True:
+                entry = pickle.load(fp)
+                try:
+                    orig_paths, revnum, author, date, message = entry
+                except:
+                    if entry is None:
+                        break
+                    raise SubversionException("child raised exception", entry)
+                yield entry
+
+        rfd, wfd = os.pipe()
+        pid = os.fork()
+        if pid:
+            os.close(wfd)
+            for p in parent(os.fdopen(rfd, 'rb')):
+                yield p
+            ret = os.waitpid(pid, 0)[1]
+            if ret:
+                raise util.Abort(_('get_log %s') % util.explain_exit(ret))
+        else:
+            os.close(rfd)
+            child(os.fdopen(wfd, 'wb'))
+            os._exit(0)
+
     def gettags(self):
         tags = {}
-        def parselogentry(*arg, **args):
-            orig_paths, revnum, author, date, message, pool = arg
-            for path in orig_paths:
-                if not path.startswith('/tags/'):
-                    continue
-                ent = orig_paths[path]
-                source = ent.copyfrom_path
-                rev = ent.copyfrom_rev
-                tag = path.split('/', 2)[2]
-                tags[tag] = self.revid(rev, module=source)
-
         start = self.revnum(self.head)
         try:
-            svn.ra.get_log(self.ra, ['/tags'], 0, start, 0, True, False,
-                           parselogentry)
-            return tags
-        except SubversionException:
+            for entry in self.get_log(['/tags'], 0, start):
+                orig_paths, revnum, author, date, message = entry
+                for path in orig_paths:
+                    if not path.startswith('/tags/'):
+                        continue
+                    ent = orig_paths[path]
+                    source = ent.copyfrom_path
+                    rev = ent.copyfrom_rev
+                    tag = path.split('/', 2)[2]
+                    tags[tag] = self.revid(rev, module=source)
+        except SubversionException, (_, num):
             self.ui.note('no tags found at revision %d\n' % start)
-            return {}
+        return tags
 
     # -- helper functions --
 
@@ -193,8 +285,8 @@ class convert_svn(converter_source):
         except SubversionException:
             dirent = None
         if not dirent:
-            raise util.Abort('%s not found up to revision %d' \
-                             % (path, stop))
+            print self.base, path
+            raise util.Abort('%s not found up to revision %d' % (path, stop))
 
         return dirent.created_rev
 
@@ -242,25 +334,10 @@ class convert_svn(converter_source):
             self.ui.debug('Ignoring %r since it is not under %r\n' % (path, module))
             return None
 
-        received = []
-        # svn.ra.get_log requires no other calls to the ra until it completes,
-        # so we just collect the log entries and parse them afterwards
-        def receivelog(*arg, **args):
-            received.append(arg)
-
         self.child_cset = None
-        def parselogentry(*arg, **args):
-            orig_paths, revnum, author, date, message, pool = arg
-
-            if self.is_blacklisted(revnum):
-                self.ui.note('skipping blacklisted revision %d\n' % revnum)
-                return
-
-            self.ui.debug("parsing revision %d\n" % revnum)
-           
-            if orig_paths is None:
-                self.ui.debug('revision %d has no entries\n' % revnum)
-                return
+        def parselogentry(orig_paths, revnum, author, date, message):
+            self.ui.debug("parsing revision %d (%d changes)\n" %
+                          (revnum, len(orig_paths)))
 
             if revnum in self.modulemap:
                 new_module = self.modulemap[revnum]
@@ -286,12 +363,11 @@ class convert_svn(converter_source):
             except IndexError:
                 branch = None
 
-            paths = orig_paths.keys()
-            paths.sort()
-            for path in paths:
+            orig_paths = orig_paths.items()
+            orig_paths.sort()
+            for path, ent in orig_paths:
                 # self.ui.write("path %s\n" % path)
                 if path == self.module: # Follow branching back in history
-                    ent = orig_paths[path]
                     if ent:
                         if ent.copyfrom_path:
                             # ent.copyfrom_rev may not be the actual last revision
@@ -310,7 +386,6 @@ class convert_svn(converter_source):
                     self.ui.debug("boring@%s: %s\n" % (revnum, path))
                     continue
                 entry = entrypath.decode(self.encoding)
-                ent = orig_paths[path]
 
                 kind = svn.ra.check_path(self.ra, entrypath, revnum)
                 if kind == svn.core.svn_node_file:
@@ -373,7 +448,7 @@ class convert_svn(converter_source):
                         # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
                         # Sometimes this is tricky. For example: in
                         # The Subversion Repository revision 6940 a dir
-                        # was copied and one of its files was deleted 
+                        # was copied and one of its files was deleted
                         # from the new location in the same commit. This
                         # code can't deal with that yet.
                         if ent.action == 'C':
@@ -387,7 +462,7 @@ class convert_svn(converter_source):
                         for child in children:
                             # Can we move a child directory and its
                             # parent in the same commit? (probably can). Could
-                            # cause problems if instead of revnum -1, 
+                            # cause problems if instead of revnum -1,
                             # we have to look in (copyfrom_path, revnum - 1)
                             entrypath = get_entry_from_path("/" + child, module=old_module)
                             if entrypath:
@@ -417,7 +492,7 @@ class convert_svn(converter_source):
                     for child in children:
                         # Can we move a child directory and its
                         # parent in the same commit? (probably can). Could
-                        # cause problems if instead of revnum -1, 
+                        # cause problems if instead of revnum -1,
                         # we have to look in (copyfrom_path, revnum - 1)
                         entrypath = get_entry_from_path("/" + child, module=self.module)
                         # print child, self.module, entrypath
@@ -466,7 +541,7 @@ class convert_svn(converter_source):
 
             self.modulemap[revnum] = self.module # track backwards in time
             # a list of (filename, id) where id lets us retrieve the file.
-            # eg in git, id is the object hash. for svn it'll be the 
+            # eg in git, id is the object hash. for svn it'll be the
             self.files[rev] = zip(entries, [rev] * len(entries))
             if not entries:
                 return
@@ -480,8 +555,8 @@ class convert_svn(converter_source):
             author = author and self.recode(author) or ''
 
             cset = commit(author=author,
-                          date=util.datestr(date), 
-                          desc=log, 
+                          date=util.datestr(date),
+                          desc=log,
                           parents=parents,
                           copies=copies,
                           branch=branch,
@@ -492,20 +567,24 @@ class convert_svn(converter_source):
                 self.child_cset.parents = [rev]
             self.child_cset = cset
 
-        self.ui.note('fetching revision log for "%s" from %d to %d\n' % \
+        self.ui.note('fetching revision log for "%s" from %d to %d\n' %
                      (self.module, from_revnum, to_revnum))
 
         try:
             discover_changed_paths = True
             strict_node_history = False
-            svn.ra.get_log(self.ra, [self.module], from_revnum, to_revnum, 0,
-                           discover_changed_paths, strict_node_history,
-                           receivelog)
-            for entry in received:
-                parselogentry(*entry)
+            for entry in self.get_log([self.module], from_revnum, to_revnum):
+                orig_paths, revnum, author, date, message = entry
+                if self.is_blacklisted(revnum):
+                    self.ui.note('skipping blacklisted revision %d\n' % revnum)
+                    continue
+                if orig_paths is None:
+                    self.ui.debug('revision %d has no entries\n' % revnum)
+                    continue
+                parselogentry(orig_paths, revnum, author, date, message)
         except SubversionException, (_, num):
             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
-                raise NoSuchRevision(branch=self, 
+                raise NoSuchRevision(branch=self,
                     revision="Revision number %d" % to_revnum)
             raise
 
@@ -567,7 +646,6 @@ class convert_svn(converter_source):
             dirents = getdir[0]
             if type(dirents) == int:
                 # got here once due to infinite recursion bug
-                # pprint.pprint(getdir)
                 return
             c = dirents.keys()
             c.sort()
--- a/hgext/convert/transport.py
+++ b/hgext/convert/transport.py
@@ -24,9 +24,10 @@ from tempfile import mktemp
 
 from svn.core import SubversionException, Pool
 import svn.ra
+import svn.client
 import svn.core
 
-# Some older versions of the Python bindings need to be 
+# Some older versions of the Python bindings need to be
 # explicitly initialized. But what we want to do probably
 # won't work worth a darn against those libraries anyway!
 svn.ra.initialize()
@@ -48,21 +49,6 @@ def _create_auth_baton(pool):
         ]
     return svn.core.svn_auth_open(providers, pool)
 
-
-#    # The SVN libraries don't like trailing slashes...
-#    return url.rstrip('/')
-
-
-class SvnRaCallbacks(svn.ra.callbacks2_t):
-    """Remote access callbacks implementation for bzr-svn."""
-    def __init__(self, pool):
-        svn.ra.callbacks2_t.__init__(self)
-        self.auth_baton = _create_auth_baton(pool)
-        self.pool = pool
-    
-    def open_tmp_file(self, pool):
-        return mktemp(prefix='tailor-svn')
-
 class NotBranchError(SubversionException):
     pass
 
@@ -73,25 +59,30 @@ class SvnRaTransport(object):
     def __init__(self, url="", ra=None):
         self.pool = Pool()
         self.svn_url = url
+        self.username = ''
+        self.password = ''
 
         # Only Subversion 1.4 has reparent()
         if ra is None or not hasattr(svn.ra, 'reparent'):
-            self.callbacks = SvnRaCallbacks(self.pool)
+            self.client = svn.client.create_context(self.pool)
+            ab = _create_auth_baton(self.pool)
+            if False:
+                svn.core.svn_auth_set_parameter(
+                    ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username)
+                svn.core.svn_auth_set_parameter(
+                    ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password)
+            self.client.auth_baton = ab
+            self.client.config = svn_config
             try:
-                ver = svn.ra.version()
-                try: # Older SVN bindings
-                    self.ra = svn.ra.open2(self.svn_url.encode('utf8'), self.callbacks, None, svn_config, None)
-                except TypeError, e:
-                    self.ra = svn.ra.open2(self.svn_url.encode('utf8'), self.callbacks, svn_config, None)
+                self.ra = svn.client.open_ra_session(
+                    self.svn_url.encode('utf8'),
+                    self.client, self.pool)
             except SubversionException, (_, num):
-                if num == svn.core.SVN_ERR_RA_ILLEGAL_URL:
-                    raise NotBranchError(url)
-                if num == svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED:
-                    raise NotBranchError(url)
-                if num == svn.core.SVN_ERR_BAD_URL:
+                if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL,
+                           svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED,
+                           svn.core.SVN_ERR_BAD_URL):
                     raise NotBranchError(url)
                 raise
-
         else:
             self.ra = ra
             svn.ra.reparent(self.ra, self.svn_url.encode('utf8'))
--- a/hgext/purge.py
+++ b/hgext/purge.py
@@ -31,7 +31,7 @@ from mercurial import hg, util
 from mercurial.i18n import _
 import os
 
-def dopurge(ui, repo, dirs=None, act=True, ignored=False, 
+def dopurge(ui, repo, dirs=None, act=True, ignored=False,
             abort_on_err=False, eol='\n',
             force=False, include=None, exclude=None):
     def error(msg):
--- a/mercurial/archival.py
+++ b/mercurial/archival.py
@@ -200,8 +200,9 @@ def archive(repo, dest, node, kind, deco
 
     prefix is name of path to put before every archive member.'''
 
-    def write(name, mode, islink, data):
+    def write(name, mode, islink, getdata):
         if matchfn and not matchfn(name): return
+        data = getdata()
         if decode:
             data = repo.wwritedata(name, data)
         archiver.addfile(name, mode, islink, data)
@@ -212,8 +213,8 @@ def archive(repo, dest, node, kind, deco
     items = m.items()
     items.sort()
     write('.hg_archival.txt', 0644, False,
-          'repo: %s\nnode: %s\n' % (hex(repo.changelog.node(0)), hex(node)))
+          lambda: 'repo: %s\nnode: %s\n' % (hex(repo.changelog.node(0)), hex(node)))
     for filename, filenode in items:
         write(filename, m.execf(filename) and 0755 or 0644, m.linkf(filename),
-              repo.file(filename).read(filenode))
+              lambda: repo.file(filename).read(filenode))
     archiver.done()
--- a/mercurial/changelog.py
+++ b/mercurial/changelog.py
@@ -42,7 +42,7 @@ class appender:
     def flush(self):
         pass
     def close(self):
-        close(self.fp)
+        self.fp.close()
 
     def seek(self, offset, whence=0):
         '''virtual file offset spans real file and data'''
@@ -58,7 +58,6 @@ class appender:
     def read(self, count=-1):
         '''only trick here is reads that span real file and data'''
         ret = ""
-        old_offset = self.offset
         if self.offset < self.size:
             s = self.fp.read(count)
             ret = s
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -316,7 +316,7 @@ def dispatch(ui, args, argv0=None):
         util._fallbackencoding = fallback
 
     fullargs = args
-    cmd, func, args, options, cmdoptions = parse(ui, args)
+    cmd, func, args, options, cmdoptions = parse(lui, args)
 
     if options["config"]:
         raise util.Abort(_("Option --config may not be abbreviated!"))
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -8,7 +8,7 @@
 import demandimport; demandimport.enable()
 from node import *
 from i18n import _
-import bisect, os, re, sys, urllib, shlex, stat
+import bisect, os, re, sys, urllib, stat
 import ui, hg, util, revlog, bundlerepo, extensions
 import difflib, patch, time, help, mdiff, tempfile
 import errno, version, socket
@@ -1362,7 +1362,7 @@ def help_(ui, name=None, with_version=Fa
 
             addglobalopts(False)
 
-    def helplist(select=None):
+    def helplist(header, select=None):
         h = {}
         cmds = {}
         for c, e in table.items():
@@ -1380,6 +1380,11 @@ def help_(ui, name=None, with_version=Fa
             h[f] = doc.splitlines(0)[0].rstrip()
             cmds[f] = c.lstrip("^")
 
+        if not h:
+            ui.status(_('no commands defined\n'))
+            return
+
+        ui.status(header)
         fns = h.keys()
         fns.sort()
         m = max(map(len, fns))
@@ -1429,14 +1434,10 @@ def help_(ui, name=None, with_version=Fa
         try:
             ct = mod.cmdtable
         except AttributeError:
-            ct = None
-        if not ct:
-            ui.status(_('no commands defined\n'))
-            return
-
-        ui.status(_('list of commands:\n\n'))
+            ct = {}
+
         modcmds = dict.fromkeys([c.split('|', 1)[0] for c in ct])
-        helplist(modcmds.has_key)
+        helplist(_('list of commands:\n\n'), modcmds.has_key)
 
     if name and name != 'shortlist':
         i = None
@@ -1460,11 +1461,11 @@ def help_(ui, name=None, with_version=Fa
 
         # list of commands
         if name == "shortlist":
-            ui.status(_('basic commands:\n\n'))
+            header = _('basic commands:\n\n')
         else:
-            ui.status(_('list of commands:\n\n'))
-
-        helplist()
+            header = _('list of commands:\n\n')
+
+        helplist(header)
 
     # list all option lists
     opt_output = []
@@ -2040,14 +2041,12 @@ def paths(ui, repo, search=None):
         for name, path in ui.configitems("paths"):
             ui.write("%s = %s\n" % (name, path))
 
-def postincoming(ui, repo, modheads, optupdate, wasempty):
+def postincoming(ui, repo, modheads, optupdate):
     if modheads == 0:
         return
     if optupdate:
-        if wasempty:
-            return hg.update(repo, repo.lookup('default'))
-        elif modheads == 1:
-            return hg.update(repo, repo.changelog.tip()) # update
+        if modheads == 1:
+            return hg.update(repo, None)
         else:
             ui.status(_("not updating, since new heads added\n"))
     if modheads > 1:
@@ -2108,9 +2107,8 @@ def pull(ui, repo, source="default", **o
             error = _("Other repository doesn't support revision lookup, so a rev cannot be specified.")
             raise util.Abort(error)
 
-    wasempty = repo.changelog.count() == 0
     modheads = repo.pull(other, heads=revs, force=opts['force'])
-    return postincoming(ui, repo, modheads, opts['update'], wasempty)
+    return postincoming(ui, repo, modheads, opts['update'])
 
 def push(ui, repo, dest=None, **opts):
     """push changes to the specified destination
@@ -2211,7 +2209,6 @@ def remove(ui, repo, *pats, **opts):
     Modified files and added files are not removed by default.  To
     remove them, use the -f/--force option.
     """
-    names = []
     if not opts['after'] and not pats:
         raise util.Abort(_('no files specified'))
     files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
@@ -2681,8 +2678,6 @@ def unbundle(ui, repo, fname1, *fnames, 
     bundle command.
     """
     fnames = (fname1,) + fnames
-    result = None
-    wasempty = repo.changelog.count() == 0
     for fname in fnames:
         if os.path.exists(fname):
             f = open(fname, "rb")
@@ -2691,7 +2686,7 @@ def unbundle(ui, repo, fname1, *fnames, 
         gen = changegroup.readbundle(f, fname)
         modheads = repo.addchangegroup(gen, 'unbundle', 'bundle:' + fname)
 
-    return postincoming(ui, repo, modheads, opts['update'], wasempty)
+    return postincoming(ui, repo, modheads, opts['update'])
 
 def update(ui, repo, node=None, rev=None, clean=False, date=None):
     """update working directory
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -21,6 +21,7 @@ class dirstate(object):
         self._opener = opener
         self._root = root
         self._dirty = False
+        self._dirtypl = False
         self._ui = ui
 
     def __getattr__(self, name):
@@ -113,7 +114,7 @@ class dirstate(object):
         return self._branch
 
     def setparents(self, p1, p2=nullid):
-        self._dirty = True
+        self._dirty = self._dirtypl = True
         self._pl = p1, p2
 
     def setbranch(self, branch):
@@ -123,7 +124,8 @@ class dirstate(object):
     def _read(self):
         self._map = {}
         self._copymap = {}
-        self._pl = [nullid, nullid]
+        if not self._dirtypl:
+            self._pl = [nullid, nullid]
         try:
             st = self._opener("dirstate").read()
         except IOError, err:
@@ -132,7 +134,8 @@ class dirstate(object):
         if not st:
             return
 
-        self._pl = [st[:20], st[20: 40]]
+        if not self._dirtypl:
+            self._pl = [st[:20], st[20: 40]]
 
         # deref fields so they will be local in loop
         dmap = self._map
@@ -157,8 +160,8 @@ class dirstate(object):
 
     def invalidate(self):
         for a in "_map _copymap _branch _pl _dirs _ignore".split():
-            if hasattr(self, a):
-                self.__delattr__(a)
+            if a in self.__dict__:
+                delattr(self, a)
         self._dirty = False
 
     def copy(self, source, dest):
@@ -271,7 +274,7 @@ class dirstate(object):
         st = self._opener("dirstate", "w", atomictemp=True)
         st.write(cs.getvalue())
         st.rename()
-        self._dirty = False
+        self._dirty = self._dirtypl = False
 
     def _filter(self, files):
         ret = {}
--- a/mercurial/hgweb/hgwebdir_mod.py
+++ b/mercurial/hgweb/hgwebdir_mod.py
@@ -244,7 +244,7 @@ class hgwebdir(object):
                     if up < 0:
                         break
                     virtual = virtual[:up]
-                
+
                 req.write(tmpl("notfound", repo=virtual))
             else:
                 if req.form.has_key('static'):
--- a/mercurial/hgweb/server.py
+++ b/mercurial/hgweb/server.py
@@ -39,7 +39,7 @@ class _error_logger(object):
 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
 
     url_scheme = 'http'
-    
+
     def __init__(self, *args, **kargs):
         self.protocol_version = 'HTTP/1.1'
         BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
@@ -173,7 +173,7 @@ class _hgwebhandler(object, BaseHTTPServ
 class _shgwebhandler(_hgwebhandler):
 
     url_scheme = 'https'
-    
+
     def setup(self):
         self.connection = self.request
         self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
--- a/mercurial/lock.py
+++ b/mercurial/lock.py
@@ -29,14 +29,13 @@ class lock(object):
     # old-style lock: symlink to pid
     # new-style lock: symlink to hostname:pid
 
+    _host = None
+
     def __init__(self, file, timeout=-1, releasefn=None, desc=None):
         self.f = file
         self.held = 0
         self.timeout = timeout
         self.releasefn = releasefn
-        self.id = None
-        self.host = None
-        self.pid = None
         self.desc = desc
         self.lock()
 
@@ -59,13 +58,12 @@ class lock(object):
                                inst.locker)
 
     def trylock(self):
-        if self.id is None:
-            self.host = socket.gethostname()
-            self.pid = os.getpid()
-            self.id = '%s:%s' % (self.host, self.pid)
+        if lock._host is None:
+            lock._host = socket.gethostname()
+        lockname = '%s:%s' % (lock._host, os.getpid())
         while not self.held:
             try:
-                util.makelock(self.id, self.f)
+                util.makelock(lockname, self.f)
                 self.held = 1
             except (OSError, IOError), why:
                 if why.errno == errno.EEXIST:
@@ -93,7 +91,7 @@ class lock(object):
             host, pid = locker.split(":", 1)
         except ValueError:
             return locker
-        if host != self.host:
+        if host != lock._host:
             return locker
         try:
             pid = int(pid)
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -281,7 +281,7 @@ def externalpatch(patcher, args, patchna
 def internalpatch(patchname, ui, strip, cwd, files):
     """use builtin patch to apply <patchname> to the working directory.
     returns whether patch was applied with fuzz factor."""
-    fp = file(patchname)
+    fp = file(patchname, 'rb')
     if cwd:
         curdir = os.getcwd()
         os.chdir(cwd)
@@ -303,7 +303,7 @@ class patchfile:
         self.fname = fname
         self.ui = ui
         try:
-            fp = file(fname, 'r')
+            fp = file(fname, 'rb')
             self.lines = fp.readlines()
             self.exists = True
         except IOError:
@@ -383,7 +383,7 @@ class patchfile:
         try: os.unlink(fname)
         except:
             pass
-        fp = file(fname, 'w')
+        fp = file(fname, 'wb')
         base = os.path.basename(self.fname)
         fp.write("--- %s\n+++ %s\n" % (base, base))
         for x in self.rej:
@@ -402,7 +402,7 @@ class patchfile:
                 if st.st_nlink > 1:
                     os.unlink(dest)
             except: pass
-            fp = file(dest, 'w')
+            fp = file(dest, 'wb')
             if st:
                 os.chmod(dest, st.st_mode)
             fp.writelines(self.lines)
@@ -777,13 +777,13 @@ def selectfile(afile_orig, bfile_orig, h
         if count == 0:
             return path.rstrip()
         while count > 0:
-            i = path.find(os.sep, i)
+            i = path.find('/', i)
             if i == -1:
                 raise PatchError(_("unable to strip away %d dirs from %s") %
                                  (count, path))
             i += 1
             # consume '//' in the path
-            while i < pathlen - 1 and path[i] == os.sep:
+            while i < pathlen - 1 and path[i] == '/':
                 i += 1
             count -= 1
         return path[i:].rstrip()
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -616,7 +616,7 @@ def rename(src, dst):
     """forcibly rename a file"""
     try:
         os.rename(src, dst)
-    except OSError, err:
+    except OSError, err: # FIXME: check err (EEXIST ?)
         # on windows, rename to existing file is not allowed, so we
         # must delete destination first. but if file is open, unlink
         # schedules it for delete but does not delete it. rename
@@ -1303,7 +1303,11 @@ class opener(object):
             os.makedirs(dirname)
 
         if self._can_symlink:
-            os.symlink(src, linkname)
+            try:
+                os.symlink(src, linkname)
+            except OSError, err:
+                raise OSError(err.errno, _('could not symlink to %r: %s') %
+                              (src, err.strerror), linkname)
         else:
             f = self(self, dst, "w")
             f.write(src)
--- a/mercurial/util_win32.py
+++ b/mercurial/util_win32.py
@@ -209,9 +209,9 @@ class posixfile_nt(object):
 
     def __init__(self, name, mode='rb'):
         access = 0
-        if 'r' in mode or '+' in mode:
+        if 'r' in mode:
             access |= win32file.GENERIC_READ
-        if 'w' in mode or 'a' in mode:
+        if 'w' in mode or 'a' in mode or '+' in mode:
             access |= win32file.GENERIC_WRITE
         if 'r' in mode:
             creation = win32file.OPEN_EXISTING
--- a/tests/test-extension
+++ b/tests/test-extension
@@ -64,3 +64,18 @@ emptypath=`pwd`/empty.py
 echo '[extensions]' > $HGRCPATH
 echo "empty = $emptypath" >> $HGRCPATH
 hg help empty
+
+cat > debugextension.py <<EOF
+'''only debugcommands
+'''
+def debugfoobar(ui, repo, *args, **opts):
+    "yet another debug command"
+    pass
+
+cmdtable = {"debugfoobar": (debugfoobar, (), "hg debugfoobar")}
+EOF
+debugpath=`pwd`/debugextension.py
+echo '[extensions]' > $HGRCPATH
+echo "debugextension = $debugpath" >> $HGRCPATH
+hg help debugextension
+hg --debug help debugextension
--- a/tests/test-extension.out
+++ b/tests/test-extension.out
@@ -22,3 +22,30 @@ Foo
 empty extension - empty cmdtable
 
 no commands defined
+debugextension extension - only debugcommands
+
+no commands defined
+debugextension extension - only debugcommands
+
+list of commands:
+
+ debugfoobar:
+      yet another debug command
+
+global options:
+ -R --repository      repository root directory or symbolic path name
+    --cwd             change working directory
+ -y --noninteractive  do not prompt, assume 'yes' for any required answers
+ -q --quiet           suppress output
+ -v --verbose         enable additional output
+    --config          set/override config option
+    --debug           enable debugging output
+    --debugger        start debugger
+    --encoding        set the charset encoding (default: ascii)
+    --encodingmode    set the charset encoding mode (default: strict)
+    --lsprof          print improved command execution profile
+    --traceback       print traceback on exception
+    --time            time how long the command takes
+    --profile         print command execution profile
+    --version         output version information and exit
+ -h --help            display help and exit
--- a/tests/test-tag
+++ b/tests/test-tag
@@ -29,14 +29,18 @@ newline'
 hg tag -l 'xx:xx'
 
 echo % issue 601
-mv .hg/localtags .hg/ltags
-head -1 .hg/ltags | tr -d '\n' > .hg/localtags
+python << EOF
+f = file('.hg/localtags'); last = f.readlines()[-1][:-1]; f.close()
+f = file('.hg/localtags', 'w'); f.write(last); f.close()
+EOF
 cat .hg/localtags
 hg tag -l localnewline
 cat .hg/localtags
 
-mv .hgtags hgtags
-head -1 hgtags | tr -d '\n' > .hgtags
+python << EOF
+f = file('.hgtags'); last = f.readlines()[-1][:-1]; f.close()
+f = file('.hgtags', 'w'); f.write(last); f.close()
+EOF
 hg ci -d '1000000 0' -m'broken manual edit of .hgtags'
 cat .hgtags
 hg tag -d '1000000 0' newline