changeset 7619:e9da3de01e0a

merge with crew-stable
author Dirkjan Ochtman <dirkjan@ochtman.nl>
date Mon, 12 Jan 2009 09:13:45 +0100
parents 6c89dd0a7797 (diff) f9fcb189c8e2 (current diff)
children fbfd92d51540
files hgext/mq.py
diffstat 58 files changed, 972 insertions(+), 502 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/zsh_completion	Mon Jan 12 09:12:35 2009 +0100
+++ b/contrib/zsh_completion	Mon Jan 12 09:13:45 2009 +0100
@@ -4,14 +4,13 @@
 # it into your zsh function path (/usr/share/zsh/site-functions for
 # instance)
 #
-# Copyright (C) 2005 Steve Borho
+# Copyright (C) 2005-6 Steve Borho
 # Copyright (C) 2006-8 Brendan Cully <brendan@kublai.com>
 #
 # This is free software; you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free
 # Software Foundation; either version 2 of the License, or (at your
 # option) any later version.
-#
 
 emulate -LR zsh
 setopt extendedglob
@@ -118,27 +117,17 @@
   typeset -ga _hg_cmd_list
   typeset -gA _hg_alias_list
   local hline cmd cmdalias
-  _call_program help hg --verbose help | while read -A hline
+
+  _call_program hg hg debugcomplete -v 2>/dev/null | while read -A hline
   do
-    cmd="$hline[1]"
-    case $cmd in
-      *:)
-        cmd=${cmd%:}
-        _hg_cmd_list+=($cmd)
-      ;;
-      *,)
-        cmd=${cmd%,}
-        _hg_cmd_list+=($cmd)
-        integer i=2
-        while (( i <= $#hline ))
-        do
-          cmdalias=${hline[$i]%(:|,)}
-          _hg_cmd_list+=($cmdalias)
-          _hg_alias_list+=($cmdalias $cmd)
-          (( i++ ))
-        done
-      ;;
-    esac
+    cmd=$hline[1]
+    _hg_cmd_list+=($cmd)
+
+    for cmdalias in $hline[2,-1]
+    do
+      _hg_cmd_list+=($cmdalias)
+      _hg_alias_list+=($cmdalias $cmd)
+    done
   done
 }
 
--- a/hgext/bookmarks.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/bookmarks.py	Mon Jan 12 09:13:45 2009 +0100
@@ -14,9 +14,19 @@
 
 It is possible to use bookmark names in every revision lookup (e.g. hg
 merge, hg update).
+
+The bookmark extension offers the possiblity to have a more git-like experience
+by adding the following configuration option to your .hgrc:
+
+[bookmarks]
+track.current = True
+
+This will cause bookmarks to track the bookmark that you are currently on, and
+just updates it. This is similar to git's approach of branching.
 '''
 
 from mercurial.commands import templateopts, hex, short
+from mercurial import extensions
 from mercurial.i18n import _
 from mercurial import cmdutil, util, commands, changelog
 from mercurial.node import nullid, nullrev
@@ -54,11 +64,54 @@
     '''
     if os.path.exists(repo.join('bookmarks')):
         util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
+    if current(repo) not in refs:
+        setcurrent(repo, None)
     file = repo.opener('bookmarks', 'w+')
     for refspec, node in refs.items():
         file.write("%s %s\n" % (hex(node), refspec))
     file.close()
 
+def current(repo):
+    '''Get the current bookmark
+
+    If we use gittishsh branches we have a current bookmark that
+    we are on. This function returns the name of the bookmark. It
+    is stored in .hg/bookmarks.current
+    '''
+    if repo._bookmarkcurrent:
+        return repo._bookmarkcurrent
+    mark = None
+    if os.path.exists(repo.join('bookmarks.current')):
+        file = repo.opener('bookmarks.current')
+        mark = file.readline()
+        if mark == '':
+            mark = None
+        file.close()
+    repo._bookmarkcurrent = mark
+    return mark
+
+def setcurrent(repo, mark):
+    '''Set the name of the bookmark that we are currently on
+
+    Set the name of the bookmark that we are on (hg update <bookmark>).
+    The name is recoreded in .hg/bookmarks.current
+    '''
+    if current(repo) == mark:
+        return
+
+    refs = parse(repo)
+
+    # do not update if we do update to a rev equal to the current bookmark
+    if (mark not in refs and
+        current(repo) and refs[current(repo)] == repo.changectx('.').node()):
+        return
+    if mark not in refs:
+        mark = ''
+    file = repo.opener('bookmarks.current', 'w+')
+    file.write(mark)
+    file.close()
+    repo._bookmarkcurrent = mark
+
 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
     '''mercurial bookmarks
 
@@ -85,6 +138,8 @@
             raise util.Abort(_("new bookmark name required"))
         marks[mark] = marks[rename]
         del marks[rename]
+        if current(repo) == rename:
+            setcurrent(repo, mark)
         write(repo, marks)
         return
 
@@ -121,7 +176,11 @@
             ui.status("no bookmarks set\n")
         else:
             for bmark, n in marks.iteritems():
-                prefix = (n == cur) and '*' or ' '
+                if ui.configbool('bookmarks', 'track.current'):
+                    prefix = (bmark == current(repo) and n == cur) and '*' or ' '
+                else:
+                    prefix = (n == cur) and '*' or ' '
+
                 ui.write(" %s %-25s %d:%s\n" % (
                     prefix, bmark, repo.changelog.rev(n), hexfn(n)))
         return
@@ -166,6 +225,7 @@
     # init a bookmark cache as otherwise we would get a infinite reading
     # in lookup()
     repo._bookmarks = None
+    repo._bookmarkcurrent = None
 
     class bookmark_repo(repo.__class__):
         def rollback(self):
@@ -192,9 +252,14 @@
             marks = parse(repo)
             update = False
             for mark, n in marks.items():
-                if n in parents:
-                    marks[mark] = node
-                    update = True
+                if ui.configbool('bookmarks', 'track.current'):
+                    if mark == current(repo) and n in parents:
+                        marks[mark] = node
+                        update = True
+                else:
+                    if n in parents:
+                        marks[mark] = node
+                        update = True
             if update:
                 write(repo, marks)
             return node
@@ -218,8 +283,35 @@
                 write(repo, marks)
             return result
 
+        def tags(self):
+            """Merge bookmarks with normal tags"""
+            if self.tagscache:
+                return self.tagscache
+
+            tagscache = super(bookmark_repo, self).tags()
+            tagscache.update(parse(repo))
+            return tagscache
+
     repo.__class__ = bookmark_repo
 
+def updatecurbookmark(orig, ui, repo, *args, **opts):
+    '''Set the current bookmark
+
+    If the user updates to a bookmark we update the .hg/bookmarks.current
+    file.
+    '''
+    res = orig(ui, repo, *args, **opts)
+    rev = opts['rev']
+    if not rev and len(args) > 0:
+        rev = args[0]
+    setcurrent(repo, rev)
+    return res
+
+def uisetup(ui):
+    'Replace push with a decorator to provide --non-bookmarked option'
+    if ui.configbool('bookmarks', 'track.current'):
+        extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
+
 cmdtable = {
     "bookmarks":
         (bookmark,
--- a/hgext/bugzilla.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/bugzilla.py	Mon Jan 12 09:13:45 2009 +0100
@@ -4,53 +4,111 @@
 #
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
-#
-# hook extension to update comments of bugzilla bugs when changesets
-# that refer to bugs by id are seen.  this hook does not change bug
-# status, only comments.
-#
-# to configure, add items to '[bugzilla]' section of hgrc.
-#
-# to use, configure bugzilla extension and enable like this:
-#
-#   [extensions]
-#   hgext.bugzilla =
-#
-#   [hooks]
-#   # run bugzilla hook on every change pulled or pushed in here
-#   incoming.bugzilla = python:hgext.bugzilla.hook
-#
-# config items:
-#
-# section name is 'bugzilla'.
-#  [bugzilla]
-#
-# REQUIRED:
-#   host = bugzilla # mysql server where bugzilla database lives
-#   password = **   # user's password
-#   version = 2.16  # version of bugzilla installed
-#
-# OPTIONAL:
-#   bzuser = ...    # fallback bugzilla user name to record comments with
-#   db = bugs       # database to connect to
-#   notify = ...    # command to run to get bugzilla to send mail
-#   regexp = ...    # regexp to match bug ids (must contain one "()" group)
-#   strip = 0       # number of slashes to strip for url paths
-#   style = ...     # style file to use when formatting comments
-#   template = ...  # template to use when formatting comments
-#   timeout = 5     # database connection timeout (seconds)
-#   user = bugs     # user to connect to database as
-#   [web]
-#   baseurl = http://hgserver/... # root of hg web site for browsing commits
-#
-# if hg committer names are not same as bugzilla user names, use
-# "usermap" feature to map from committer email to bugzilla user name.
-# usermap can be in hgrc or separate config file.
-#
-#   [bugzilla]
-#   usermap = filename # cfg file with "committer"="bugzilla user" info
-#   [usermap]
-#   committer_email = bugzilla_user_name
+
+'''Bugzilla integration
+
+This hook extension adds comments on bugs in Bugzilla when changesets
+that refer to bugs by Bugzilla ID are seen. The hook does not change bug
+status.
+
+The hook updates the Bugzilla database directly. Only Bugzilla installations
+using MySQL are supported.
+
+The hook relies on a Bugzilla script to send bug change notification emails.
+That script changes between Bugzilla versions; the 'processmail' script used
+prior to 2.18 is replaced in 2.18 and subsequent versions by
+'config/sendbugmail.pl'. Note that these will be run by Mercurial as the user
+pushing the change; you will need to ensure the Bugzilla install file
+permissions are set appropriately.
+
+Configuring the extension:
+
+    [bugzilla]
+    host       Hostname of the MySQL server holding the Bugzilla database.
+    db         Name of the Bugzilla database in MySQL. Default 'bugs'.
+    user       Username to use to access MySQL server. Default 'bugs'.
+    password   Password to use to access MySQL server.
+    timeout    Database connection timeout (seconds). Default 5.
+    version    Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and
+               later, '2.18' for Bugzilla versions from 2.18 and '2.16' for
+               versions prior to 2.18.
+    bzuser     Fallback Bugzilla user name to record comments with, if
+               changeset committer cannot be found as a Bugzilla user.
+    bzdir      Bugzilla install directory. Used by default notify.
+               Default '/var/www/html/bugzilla'.
+    notify     The command to run to get Bugzilla to send bug change
+               notification emails. Substitutes from a map with 3 keys,
+               'bzdir', 'id' (bug id) and 'user' (committer bugzilla email).
+               Default depends on version; from 2.18 it is
+               "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s".
+    regexp     Regular expression to match bug IDs in changeset commit message.
+               Must contain one "()" group. The default expression matches
+               'Bug 1234', 'Bug no. 1234', 'Bug number 1234',
+               'Bugs 1234,5678', 'Bug 1234 and 5678' and variations thereof.
+               Matching is case insensitive.
+    style      The style file to use when formatting comments.
+    template   Template to use when formatting comments. Overrides
+               style if specified. In addition to the usual Mercurial
+               keywords, the extension specifies:
+                   {bug}       The Bugzilla bug ID.
+                   {root}      The full pathname of the Mercurial repository.
+                   {webroot}   Stripped pathname of the Mercurial repository.
+                   {hgweb}     Base URL for browsing Mercurial repositories.
+               Default 'changeset {node|short} in repo {root} refers '
+                       'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
+    strip      The number of slashes to strip from the front of {root}
+               to produce {webroot}. Default 0.
+    usermap    Path of file containing Mercurial committer ID to Bugzilla user
+               ID mappings. If specified, the file should contain one mapping
+               per line, "committer"="Bugzilla user". See also the
+               [usermap] section.
+
+    [usermap]
+    Any entries in this section specify mappings of Mercurial committer ID
+    to Bugzilla user ID. See also [bugzilla].usermap.
+    "committer"="Bugzilla user"
+
+    [web]
+    baseurl    Base URL for browsing Mercurial repositories. Reference from
+               templates as {hgweb}.
+
+Activating the extension:
+
+    [extensions]
+    hgext.bugzilla =
+
+    [hooks]
+    # run bugzilla hook on every change pulled or pushed in here
+    incoming.bugzilla = python:hgext.bugzilla.hook
+
+Example configuration:
+
+This example configuration is for a collection of Mercurial repositories
+in /var/local/hg/repos/ used with a local Bugzilla 3.2 installation in
+/opt/bugzilla-3.2.
+
+    [bugzilla]
+    host=localhost
+    password=XYZZY
+    version=3.0
+    bzuser=unknown@domain.com
+    bzdir=/opt/bugzilla-3.2
+    template=Changeset {node|short} in {root|basename}.\\n{hgweb}/{webroot}/rev/{node|short}\\n\\n{desc}\\n
+    strip=5
+
+    [web]
+    baseurl=http://dev.domain.com/hg
+
+    [usermap]
+    user@emaildomain.com=user.name@bugzilladomain.com
+
+Commits add a comment to the Bugzilla bug record of the form:
+
+    Changeset 3b16791d6642 in repository-name.
+    http://dev.domain.com/hg/repository-name/rev/3b16791d6642
+
+    Changeset commit comment. Bug 1234.
+'''
 
 from mercurial.i18n import _
 from mercurial.node import short
@@ -82,6 +140,7 @@
         self.cursor = self.conn.cursor()
         self.longdesc_id = self.get_longdesc_id()
         self.user_ids = {}
+        self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
 
     def run(self, *args, **kwargs):
         '''run a query.'''
@@ -118,15 +177,23 @@
             unknown.pop(id, None)
         return util.sort(unknown.keys())
 
-    def notify(self, ids):
+    def notify(self, ids, committer):
         '''tell bugzilla to send mail.'''
 
         self.ui.status(_('telling bugzilla to send mail:\n'))
+        (user, userid) = self.get_bugzilla_user(committer)
         for id in ids:
             self.ui.status(_('  bug %s\n') % id)
-            cmd = self.ui.config('bugzilla', 'notify',
-                               'cd /var/www/html/bugzilla && '
-                               './processmail %s nobody@nowhere.com') % id
+            cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
+            bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
+            try:
+                # Backwards-compatible with old notify string, which
+                # took one string. This will throw with a new format
+                # string.
+                cmd = cmdfmt % id
+            except TypeError:
+                cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
+            self.ui.note(_('running notify command %s\n') % cmd)
             fp = util.popen('(%s) 2>&1' % cmd)
             out = fp.read()
             ret = fp.close()
@@ -161,9 +228,10 @@
                 return bzuser
         return user
 
-    def add_comment(self, bugid, text, committer):
-        '''add comment to bug. try adding comment as committer of
-        changeset, otherwise as default bugzilla user.'''
+    def get_bugzilla_user(self, committer):
+        '''see if committer is a registered bugzilla user. Return
+        bugzilla username and userid if so. If not, return default
+        bugzilla username and userid.'''
         user = self.map_committer(committer)
         try:
             userid = self.get_user_id(user)
@@ -174,9 +242,16 @@
                     raise util.Abort(_('cannot find bugzilla user id for %s') %
                                      user)
                 userid = self.get_user_id(defaultuser)
+                user = defaultuser
             except KeyError:
                 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
                                  (user, defaultuser))
+        return (user, userid)
+
+    def add_comment(self, bugid, text, committer):
+        '''add comment to bug. try adding comment as committer of
+        changeset, otherwise as default bugzilla user.'''
+        (user, userid) = self.get_bugzilla_user(committer)
         now = time.strftime('%Y-%m-%d %H:%M:%S')
         self.run('''insert into longdescs
                     (bug_id, who, bug_when, thetext)
@@ -185,12 +260,20 @@
         self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
                     values (%s, %s, %s, %s)''',
                  (bugid, userid, now, self.longdesc_id))
+        self.conn.commit()
 
-class bugzilla_3_0(bugzilla_2_16):
+class bugzilla_2_18(bugzilla_2_16):
+    '''support for bugzilla 2.18 series.'''
+
+    def __init__(self, ui):
+        bugzilla_2_16.__init__(self, ui)
+        self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
+
+class bugzilla_3_0(bugzilla_2_18):
     '''support for bugzilla 3.0 series.'''
 
     def __init__(self, ui):
-        bugzilla_2_16.__init__(self, ui)
+        bugzilla_2_18.__init__(self, ui)
 
     def get_longdesc_id(self):
         '''get identity of longdesc field'''
@@ -205,6 +288,7 @@
     # different schemas.
     _versions = {
         '2.16': bugzilla_2_16,
+        '2.18': bugzilla_2_18,
         '3.0':  bugzilla_3_0
         }
 
@@ -320,7 +404,7 @@
         if ids:
             for id in ids:
                 bz.update(id, ctx)
-            bz.notify(ids)
+            bz.notify(ids, util.email(ctx.user()))
     except MySQLdb.MySQLError, err:
         raise util.Abort(_('database error: %s') % err[1])
 
--- a/hgext/churn.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/churn.py	Mon Jan 12 09:13:45 2009 +0100
@@ -12,27 +12,6 @@
 import os, sys
 import time, datetime
 
-def get_tty_width():
-    if 'COLUMNS' in os.environ:
-        try:
-            return int(os.environ['COLUMNS'])
-        except ValueError:
-            pass
-    try:
-        import termios, array, fcntl
-        for dev in (sys.stdout, sys.stdin):
-            try:
-                fd = dev.fileno()
-                if not os.isatty(fd):
-                    continue
-                arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
-                return array.array('h', arri)[1]
-            except ValueError:
-                pass
-    except ImportError:
-        pass
-    return 80
-
 def maketemplater(ui, repo, tmpl):
     tmpl = templater.parsestring(tmpl, quoted=False)
     try:
@@ -111,7 +90,7 @@
 
 
 def churn(ui, repo, *pats, **opts):
-    '''Graph count of revisions grouped by template
+    '''graph count of revisions grouped by template
 
     Will graph count of changed lines or revisions grouped by template or
     alternatively by date, if dateformat is used. In this case it will override
@@ -157,7 +136,7 @@
     maxcount = float(max([v for k, v in rate]))
     maxname = max([len(k) for k, v in rate])
 
-    ttywidth = get_tty_width()
+    ttywidth = util.termwidth()
     ui.debug(_("assuming %i character terminal\n") % ttywidth)
     width = ttywidth - maxname - 2 - 6 - 2 - 2
 
--- a/hgext/color.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/color.py	Mon Jan 12 09:13:45 2009 +0100
@@ -204,6 +204,7 @@
 _diff_prefixes = [('diff', 'diffline'),
                   ('copy', 'extended'),
                   ('rename', 'extended'),
+                  ('old', 'extended'),
                   ('new', 'extended'),
                   ('deleted', 'extended'),
                   ('---', 'file_a'),
--- a/hgext/convert/__init__.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/__init__.py	Mon Jan 12 09:13:45 2009 +0100
@@ -7,13 +7,14 @@
 '''converting foreign VCS repositories to Mercurial'''
 
 import convcmd
+import cvsps
 from mercurial import commands
 from mercurial.i18n import _
 
 # Commands definition was moved elsewhere to ease demandload job.
 
 def convert(ui, src, dest=None, revmapfile=None, **opts):
-    """Convert a foreign SCM repository to a Mercurial one.
+    """convert a foreign SCM repository to a Mercurial one.
 
     Accepted source formats [identifiers]:
     - Mercurial [hg]
@@ -183,7 +184,18 @@
 def debugsvnlog(ui, **opts):
     return convcmd.debugsvnlog(ui, **opts)
 
-commands.norepo += " convert debugsvnlog"
+def debugcvsps(ui, *args, **opts):
+    '''create changeset information from CVS
+
+    This command is intended as a debugging tool for the CVS to Mercurial
+    converter, and can be used as a direct replacement for cvsps.
+
+    Hg debugcvsps reads the CVS rlog for current directory (or any named
+    directory) in the CVS repository, and converts the log to a series of
+    changesets based on matching commit log entries and dates.'''
+    return cvsps.debugcvsps(ui, *args, **opts)
+
+commands.norepo += " convert debugsvnlog debugcvsps"
 
 cmdtable = {
     "convert":
@@ -200,4 +212,22 @@
         (debugsvnlog,
          [],
          'hg debugsvnlog'),
+    "debugcvsps":
+        (debugcvsps,
+         [
+          # Main options shared with cvsps-2.1
+          ('b', 'branches', [], _('only return changes on specified branches')),
+          ('p', 'prefix', '', _('prefix to remove from file names')),
+          ('r', 'revisions', [], _('only return changes after or between specified tags')),
+          ('u', 'update-cache', None, _("update cvs log cache")),
+          ('x', 'new-cache', None, _("create new cvs log cache")),
+          ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
+          ('', 'root', '', _('specify cvsroot')),
+          # Options specific to builtin cvsps
+          ('', 'parents', '', _('show parent changesets')),
+          ('', 'ancestors', '', _('show current changeset in ancestor branches')),
+          # Options that are ignored for compatibility with cvsps-2.1
+          ('A', 'cvs-direct', None, 'ignored for compatibility'),
+         ],
+         'hg debugcvsps [OPTION]... [PATH]...'),
 }
--- a/hgext/convert/common.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/common.py	Mon Jan 12 09:13:45 2009 +0100
@@ -228,7 +228,9 @@
             except TypeError:
                 pass
         cmdline = [util.shellquote(arg) for arg in cmdline]
-        cmdline += ['2>', util.nulldev, '<', util.nulldev]
+        if not self.ui.debugflag:
+            cmdline += ['2>', util.nulldev]
+        cmdline += ['<', util.nulldev]
         cmdline = ' '.join(cmdline)
         return cmdline
 
--- a/hgext/convert/convcmd.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/convcmd.py	Mon Jan 12 09:13:45 2009 +0100
@@ -206,7 +206,7 @@
                         _('Overriding mapping for author %s, was %s, will be %s\n')
                         % (srcauthor, self.authors[srcauthor], dstauthor))
                 else:
-                    self.ui.debug(_('Mapping author %s to %s\n')
+                    self.ui.debug(_('mapping author %s to %s\n')
                                   % (srcauthor, dstauthor))
                     self.authors[srcauthor] = dstauthor
             except IndexError:
--- a/hgext/convert/cvs.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/cvs.py	Mon Jan 12 09:13:45 2009 +0100
@@ -144,11 +144,11 @@
                             if branch == "HEAD":
                                 branch = ""
                             if branch:
-                                latest = None
+                                latest = 0
                                 # the last changeset that contains a base
                                 # file is our parent
                                 for r in oldrevs:
-                                    latest = max(filerevids.get(r, None), latest)
+                                    latest = max(filerevids.get(r, 0), latest)
                                 if latest:
                                     p = [latest]
 
--- a/hgext/convert/cvsps	Mon Jan 12 09:12:35 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-#!/usr/bin/env python
-#
-# Commandline front-end for cvsps.py
-#
-# Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
-#
-# This software may be used and distributed according to the terms
-# of the GNU General Public License, incorporated herein by reference.
-
-import sys
-from mercurial import util
-from mercurial.i18n import _
-from optparse import OptionParser, SUPPRESS_HELP
-from hgext.convert.cvsps import createlog, createchangeset, logerror
-
-def main():
-    '''Main program to mimic cvsps.'''
-
-    op = OptionParser(usage='%prog [-bpruvxz] path',
-                      description='Read CVS rlog for current directory or named '
-                                  'path in repository, and convert the log to changesets '
-                                  'based on matching commit log entries and dates.')
-
-    # Options that are ignored for compatibility with cvsps-2.1
-    op.add_option('-A', dest='Ignore', action='store_true', help=SUPPRESS_HELP)
-    op.add_option('--cvs-direct', dest='Ignore', action='store_true', help=SUPPRESS_HELP)
-    op.add_option('-q', dest='Ignore', action='store_true', help=SUPPRESS_HELP)
-
-    # Main options shared with cvsps-2.1
-    op.add_option('-b', dest='Branches', action='append', default=[],
-                  help='Only return changes on specified branches')
-    op.add_option('-p', dest='Prefix', action='store', default='',
-                  help='Prefix to remove from file names')
-    op.add_option('-r', dest='Revisions', action='append', default=[],
-                  help='Only return changes after or between specified tags')
-    op.add_option('-u', dest='Cache', action='store_const', const='update',
-                  help="Update cvs log cache")
-    op.add_option('-v', dest='Verbose', action='count', default=0,
-                  help='Be verbose')
-    op.add_option('-x', dest='Cache', action='store_const', const='write',
-                  help="Create new cvs log cache")
-    op.add_option('-z', dest='Fuzz', action='store', type='int', default=60,
-                  help='Set commit time fuzz', metavar='seconds')
-    op.add_option('--root', dest='Root', action='store', default='',
-                  help='Specify cvsroot', metavar='cvsroot')
-
-    # Options specific to this version
-    op.add_option('--parents', dest='Parents', action='store_true',
-                  help='Show parent changesets')
-    op.add_option('--ancestors', dest='Ancestors', action='store_true',
-                  help='Show current changeset in ancestor branches')
-
-    options, args = op.parse_args()
-
-    # Create a ui object for printing progress messages
-    class UI:
-        def __init__(self, verbose):
-            if verbose:
-                self.status = self.message
-            if verbose>1:
-                self.note = self.message
-            if verbose>2:
-                self.debug = self.message
-        def message(self, msg):
-            sys.stderr.write(msg)
-        def nomessage(self, msg):
-            pass
-        status = nomessage
-        note = nomessage
-        debug = nomessage
-    ui = UI(options.Verbose)
-
-    try:
-        if args:
-            log = []
-            for d in args:
-                log += createlog(ui, d, root=options.Root, cache=options.Cache)
-        else:
-            log = createlog(ui, root=options.Root, cache=options.Cache)
-    except logerror, e:
-        print e
-        return
-
-    changesets = createchangeset(ui, log, options.Fuzz)
-    del log
-
-    # Print changesets (optionally filtered)
-
-    off = len(options.Revisions)
-    branches = {}    # latest version number in each branch
-    ancestors = {}   # parent branch
-    for cs in changesets:
-
-        if options.Ancestors:
-            if cs.branch not in branches and cs.parents and cs.parents[0].id:
-                ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
-            branches[cs.branch] = cs.id
-
-        # limit by branches
-        if options.Branches and (cs.branch or 'HEAD') not in options.Branches:
-            continue
-
-        if not off:
-            # Note: trailing spaces on several lines here are needed to have
-            #       bug-for-bug compatibility with cvsps.
-            print '---------------------'
-            print 'PatchSet %d ' % cs.id
-            print 'Date: %s' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2')
-            print 'Author: %s' % cs.author
-            print 'Branch: %s' % (cs.branch or 'HEAD')
-            print 'Tag%s: %s ' % (['', 's'][len(cs.tags)>1],
-                                  ','.join(cs.tags) or '(none)')
-            if options.Parents and cs.parents:
-                if len(cs.parents)>1:
-                    print 'Parents: %s' % (','.join([str(p.id) for p in cs.parents]))
-                else:
-                    print 'Parent: %d' % cs.parents[0].id
-
-            if options.Ancestors:
-                b = cs.branch
-                r = []
-                while b:
-                    b, c = ancestors[b]
-                    r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
-                if r:
-                    print 'Ancestors: %s' % (','.join(r))
-
-            print 'Log:'
-            print cs.comment
-            print
-            print 'Members: '
-            for f in cs.entries:
-                fn = f.file
-                if fn.startswith(options.Prefix):
-                    fn = fn[len(options.Prefix):]
-                print '\t%s:%s->%s%s ' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
-                                          '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead])
-            print
-
-        # have we seen the start tag?
-        if options.Revisions and off:
-            if options.Revisions[0] == str(cs.id) or \
-                options.Revisions[0] in cs.tags:
-                off = False
-
-        # see if we reached the end tag
-        if len(options.Revisions)>1 and not off:
-            if options.Revisions[1] == str(cs.id) or \
-                options.Revisions[1] in cs.tags:
-                break
-
-
-if __name__ == '__main__':
-    main()
--- a/hgext/convert/cvsps.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/cvsps.py	Mon Jan 12 09:13:45 2009 +0100
@@ -191,7 +191,13 @@
     ui.note(_("running %s\n") % (' '.join(cmd)))
     ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
 
-    for line in util.popen(' '.join(cmd)):
+    pfp = util.popen(' '.join(cmd))
+    peek = pfp.readline()
+    while True:
+        line = peek
+        if line == '':
+            break
+        peek = pfp.readline()
         if line.endswith('\n'):
             line = line[:-1]
         #ui.debug('state=%d line=%r\n' % (state, line))
@@ -263,7 +269,7 @@
             if re_31.match(line):
                 state = 5
             else:
-                assert not re_32.match(line), _('Must have at least some revisions')
+                assert not re_32.match(line), _('must have at least some revisions')
 
         elif state == 5:
             # expecting revision number and possibly (ignored) lock indication
@@ -312,7 +318,7 @@
                 e.branches = [tuple([int(y) for y in x.strip().split('.')])
                                 for x in m.group(1).split(';')]
                 state = 8
-            elif re_31.match(line):
+            elif re_31.match(line) and re_50.match(peek):
                 state = 5
                 store = True
             elif re_32.match(line):
@@ -584,3 +590,95 @@
     ui.status(_('%d changeset entries\n') % len(changesets))
 
     return changesets
+
+
+def debugcvsps(ui, *args, **opts):
+    '''Read CVS rlog for current directory or named path in repository, and
+    convert the log to changesets based on matching commit log entries and dates.'''
+
+    if opts["new_cache"]:
+        cache = "write"
+    elif opts["update_cache"]:
+        cache = "update"
+    else:
+        cache = None
+
+    revisions = opts["revisions"]
+
+    try:
+        if args:
+            log = []
+            for d in args:
+                log += createlog(ui, d, root=opts["root"], cache=cache)
+        else:
+            log = createlog(ui, root=opts["root"], cache=cache)
+    except logerror, e:
+        ui.write("%r\n"%e)
+        return
+
+    changesets = createchangeset(ui, log, opts["fuzz"])
+    del log
+
+    # Print changesets (optionally filtered)
+
+    off = len(revisions)
+    branches = {}    # latest version number in each branch
+    ancestors = {}   # parent branch
+    for cs in changesets:
+
+        if opts["ancestors"]:
+            if cs.branch not in branches and cs.parents and cs.parents[0].id:
+                ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
+            branches[cs.branch] = cs.id
+
+        # limit by branches
+        if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
+            continue
+
+        if not off:
+            # Note: trailing spaces on several lines here are needed to have
+            #       bug-for-bug compatibility with cvsps.
+            ui.write('---------------------\n')
+            ui.write('PatchSet %d \n' % cs.id)
+            ui.write('Date: %s\n' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2'))
+            ui.write('Author: %s\n' % cs.author)
+            ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
+            ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
+                                  ','.join(cs.tags) or '(none)'))
+            if opts["parents"] and cs.parents:
+                if len(cs.parents)>1:
+                    ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
+                else:
+                    ui.write('Parent: %d\n' % cs.parents[0].id)
+
+            if opts["ancestors"]:
+                b = cs.branch
+                r = []
+                while b:
+                    b, c = ancestors[b]
+                    r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
+                if r:
+                    ui.write('Ancestors: %s\n' % (','.join(r)))
+
+            ui.write('Log:\n')
+            ui.write('%s\n\n' % cs.comment)
+            ui.write('Members: \n')
+            for f in cs.entries:
+                fn = f.file
+                if fn.startswith(opts["prefix"]):
+                    fn = fn[len(opts["prefix"]):]
+                ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
+                                          '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
+            ui.write('\n')
+
+        # have we seen the start tag?
+        if revisions and off:
+            if revisions[0] == str(cs.id) or \
+                revisions[0] in cs.tags:
+                off = False
+
+        # see if we reached the end tag
+        if len(revisions)>1 and not off:
+            if revisions[1] == str(cs.id) or \
+                revisions[1] in cs.tags:
+                break
--- a/hgext/convert/darcs.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/darcs.py	Mon Jan 12 09:13:45 2009 +0100
@@ -32,7 +32,7 @@
         if ElementTree is None:
             raise util.Abort(_("Python ElementTree module is not available"))
 
-        if not os.path.exists(os.path.join(path, '_darcs', 'inventory')):
+        if not os.path.exists(os.path.join(path, '_darcs', 'inventories')):
             raise NoRepo("%s does not look like a darcs repo" % path)
 
         self.path = os.path.realpath(path)
--- a/hgext/convert/gnuarch.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/gnuarch.py	Mon Jan 12 09:13:45 2009 +0100
@@ -3,7 +3,8 @@
 from common import NoRepo, commandline, commit, converter_source
 from mercurial.i18n import _
 from mercurial import util
-import os, shutil, tempfile, stat
+import os, shutil, tempfile, stat, locale
+from email.Parser import Parser
 
 class gnuarch_source(converter_source, commandline):
 
@@ -13,6 +14,7 @@
             self.summary = ''
             self.date = None
             self.author = ''
+            self.continuationof = None
             self.add_files = []
             self.mod_files = []
             self.del_files = []
@@ -46,38 +48,74 @@
         self.parents = {}
         self.tags = {}
         self.modecache = {}
+        self.catlogparser = Parser()
+        self.locale = locale.getpreferredencoding()
+        self.archives = []
 
     def before(self):
+        # Get registered archives
+        self.archives = [i.rstrip('\n')
+                         for i in self.runlines0('archives', '-n')]
+
         if self.execmd == 'tla':
             output = self.run0('tree-version', self.path)
         else:
             output = self.run0('tree-version', '-d', self.path)
         self.treeversion = output.strip()
 
-        self.ui.status(_('analyzing tree version %s...\n') % self.treeversion)
-
         # Get name of temporary directory
         version = self.treeversion.split('/')
         self.tmppath = os.path.join(tempfile.gettempdir(),
                                     'hg-%s' % version[1])
 
         # Generate parents dictionary
-        child = []
-        output, status = self.runlines('revisions', self.treeversion)
-        self.checkexit(status, 'archive registered?')
-        for l in output:
-            rev = l.strip()
-            self.changes[rev] = self.gnuarch_rev(rev)
+        self.parents[None] = []
+        treeversion = self.treeversion
+        child = None
+        while treeversion:
+            self.ui.status(_('analyzing tree version %s...\n') % treeversion)
+
+            archive = treeversion.split('/')[0]
+            if archive not in self.archives:
+                self.ui.status(_('tree analysis stopped because it points to an unregistered archive %s...\n') % archive)
+                break
+
+            # Get the complete list of revisions for that tree version
+            output, status = self.runlines('revisions', '-r', '-f', treeversion)
+            self.checkexit(status, 'failed retrieveing revisions for %s' % treeversion)
+
+            # No new iteration unless a revision has a continuation-of header
+            treeversion = None
+
+            for l in output:
+                rev = l.strip()
+                self.changes[rev] = self.gnuarch_rev(rev)
+                self.parents[rev] = []
 
-            # Read author, date and summary
-            catlog = self.runlines0('cat-log', '-d', self.path, rev)
-            self._parsecatlog(catlog, rev)
+                # Read author, date and summary
+                catlog, status = self.run('cat-log', '-d', self.path, rev)
+                if status:
+                    catlog  = self.run0('cat-archive-log', rev)
+                self._parsecatlog(catlog, rev)
+
+                # Populate the parents map
+                self.parents[child].append(rev)
 
-            self.parents[rev] = child
-            child = [rev]
-            if rev == self.rev:
-                break
-        self.parents[None] = child
+                # Keep track of the current revision as the child of the next
+                # revision scanned
+                child = rev
+
+                # Check if we have to follow the usual incremental history
+                # or if we have to 'jump' to a different treeversion given
+                # by the continuation-of header.
+                if self.changes[rev].continuationof:
+                    treeversion = '--'.join(self.changes[rev].continuationof.split('--')[:-1])
+                    break
+
+                # If we reached a base-0 revision w/o any continuation-of
+                # header, it means the tree history ends here.
+                if rev[-6:] == 'base-0':
+                    break
 
     def after(self):
         self.ui.debug(_('cleaning up %s\n') % self.tmppath)
@@ -135,7 +173,7 @@
     def getcommit(self, rev):
         changes = self.changes[rev]
         return commit(author = changes.author, date = changes.date,
-                      desc = changes.summary, parents = self.parents[rev])
+                      desc = changes.summary, parents = self.parents[rev], rev=rev)
 
     def gettags(self):
         return self.tags
@@ -150,26 +188,19 @@
         return os.system(cmdline)
 
     def _update(self, rev):
-        if rev == 'base-0':
-            # Initialise 'base-0' revision
+        self.ui.debug(_('applying revision %s...\n') % rev)
+        changeset, status = self.runlines('replay', '-d', self.tmppath,
+                                              rev)
+        if status:
+            # Something went wrong while merging (baz or tla
+            # issue?), get latest revision and try from there
+            shutil.rmtree(self.tmppath, ignore_errors=True)
             self._obtainrevision(rev)
         else:
-            self.ui.debug(_('applying revision %s...\n') % rev)
-            revision = '%s--%s' % (self.treeversion, rev)
-            changeset, status = self.runlines('replay', '-d', self.tmppath,
-                                              revision)
-            if status:
-                # Something went wrong while merging (baz or tla
-                # issue?), get latest revision and try from there
-                shutil.rmtree(self.tmppath, ignore_errors=True)
-                self._obtainrevision(rev)
-            else:
-                old_rev = self.parents[rev][0]
-                self.ui.debug(_('computing changeset between %s and %s...\n')
-                              % (old_rev, rev))
-                rev_a = '%s--%s' % (self.treeversion, old_rev)
-                rev_b = '%s--%s' % (self.treeversion, rev)
-                self._parsechangeset(changeset, rev)
+            old_rev = self.parents[rev][0]
+            self.ui.debug(_('computing changeset between %s and %s...\n')
+                          % (old_rev, rev))
+            self._parsechangeset(changeset, rev)
 
     def _getfile(self, name, rev):
         mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
@@ -217,8 +248,7 @@
 
     def _obtainrevision(self, rev):
         self.ui.debug(_('obtaining revision %s...\n') % rev)
-        revision = '%s--%s' % (self.treeversion, rev)
-        output = self._execute('get', revision, self.tmppath)
+        output = self._execute('get', rev, self.tmppath)
         self.checkexit(output)
         self.ui.debug(_('analysing revision %s...\n') % rev)
         files = self._readcontents(self.tmppath)
@@ -230,20 +260,27 @@
         return path
 
     def _parsecatlog(self, data, rev):
-        summary = []
-        for l in data:
-            l = l.strip()
-            if summary:
-                summary.append(l)
-            elif l.startswith('Summary:'):
-                summary.append(l[len('Summary: '):])
-            elif l.startswith('Standard-date:'):
-                date = l[len('Standard-date: '):]
-                strdate = util.strdate(date, '%Y-%m-%d %H:%M:%S')
-                self.changes[rev].date = util.datestr(strdate)
-            elif l.startswith('Creator:'):
-                self.changes[rev].author = l[len('Creator: '):]
-        self.changes[rev].summary = '\n'.join(summary)
+        try:
+            catlog = self.catlogparser.parsestr(data)
+
+            # Commit date
+            self.changes[rev].date = util.datestr(
+                util.strdate(catlog['Standard-date'],
+                             '%Y-%m-%d %H:%M:%S'))
+
+            # Commit author
+            self.changes[rev].author = self.recode(catlog['Creator'])
+
+            # Commit description
+            self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
+                                                    catlog.get_payload()))
+            self.changes[rev].summary = self.recode(self.changes[rev].summary)
+
+            # Commit revision origin when dealing with a branch or tag
+            if catlog.has_key('Continuation-of'):
+                self.changes[rev].continuationof = self.recode(catlog['Continuation-of'])
+        except Exception, err:
+            raise util.Abort(_('could not parse cat-log of %s') % rev)
 
     def _parsechangeset(self, data, rev):
         for l in data:
--- a/hgext/convert/subversion.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/convert/subversion.py	Mon Jan 12 09:13:45 2009 +0100
@@ -601,7 +601,7 @@
                         part = "/".join(parts[:i])
                         info = part, copyfrom.get(part, None)
                         if info[1] is not None:
-                            self.ui.debug(_("Found parent directory %s\n") % info[1])
+                            self.ui.debug(_("found parent directory %s\n") % info[1])
                             rc = info
                     return rc
 
@@ -616,7 +616,7 @@
                     self.ui.debug(entrypath[len(frompath):] + '\n')
                     entrypath = froment.copyfrom_path + entrypath[len(frompath):]
                     fromrev = froment.copyfrom_rev
-                    self.ui.debug(_("Info: %s %s %s %s\n") % (frompath, froment, ent, entrypath))
+                    self.ui.debug(_("info: %s %s %s %s\n") % (frompath, froment, ent, entrypath))
 
                 # We can avoid the reparent calls if the module has not changed
                 # but it probably does not worth the pain.
@@ -757,7 +757,7 @@
                             self.ui.note(_('found parent of branch %s at %d: %s\n') %
                                          (self.module, prevnum, prevmodule))
                 else:
-                    self.ui.debug(_("No copyfrom path, don't know what to do.\n"))
+                    self.ui.debug(_("no copyfrom path, don't know what to do.\n"))
 
             paths = []
             # filter out unrelated paths
--- a/hgext/extdiff.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/extdiff.py	Mon Jan 12 09:13:45 2009 +0100
@@ -80,9 +80,7 @@
     '''snapshot files from working directory.
     if not using snapshot, -I/-X does not work and recursive diff
     in tools like kdiff3 and meld displays too many files.'''
-    repo_root = repo.root
-
-    dirname = os.path.basename(repo_root)
+    dirname = os.path.basename(repo.root)
     if dirname == "":
         dirname = "root"
     base = os.path.join(tmproot, dirname)
@@ -105,8 +103,7 @@
             fp.write(chunk)
         fp.close()
 
-        fns_and_mtime.append((dest, os.path.join(repo_root, fn),
-            os.path.getmtime(dest)))
+        fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
 
 
     return dirname, fns_and_mtime
@@ -169,7 +166,7 @@
 
         for copy_fn, working_fn, mtime in fns_and_mtime:
             if os.path.getmtime(copy_fn) != mtime:
-                ui.debug(_('File changed while diffing. '
+                ui.debug(_('file changed while diffing. '
                          'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
                 util.copyfile(copy_fn, working_fn)
 
--- a/hgext/fetch.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/fetch.py	Mon Jan 12 09:13:45 2009 +0100
@@ -11,7 +11,7 @@
 from mercurial import commands, cmdutil, hg, util, url
 
 def fetch(ui, repo, source='default', **opts):
-    '''Pull changes from a remote repository, merge new changes if needed.
+    '''pull changes from a remote repository, merge new changes if needed.
 
     This finds all changes from the repository at the specified path
     or URL and adds them to the local repository.
--- a/hgext/hgk.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/hgk.py	Mon Jan 12 09:13:45 2009 +0100
@@ -130,7 +130,7 @@
         ui.write('\0')
 
 def base(ui, repo, node1, node2):
-    """Output common ancestor information"""
+    """output common ancestor information"""
     node1 = repo.lookup(node1)
     node2 = repo.lookup(node2)
     n = repo.changelog.ancestor(node1, node2)
@@ -282,7 +282,7 @@
             count += 1
 
 def revparse(ui, repo, *revs, **opts):
-    """Parse given revisions"""
+    """parse given revisions"""
     def revstr(rev):
         if rev == 'HEAD':
             rev = 'tip'
--- a/hgext/keyword.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/keyword.py	Mon Jan 12 09:13:45 2009 +0100
@@ -425,14 +425,10 @@
     keyword substitutions.
     Monkeypatches patch and webcommands.'''
 
-    try:
-        if (not repo.local() or not kwtools['inc']
-            or kwtools['hgcmd'] in nokwcommands.split()
-            or '.hg' in util.splitpath(repo.root)
-            or repo._url.startswith('bundle:')):
-            return
-    except AttributeError:
-        pass
+    if (not hasattr(repo, 'dirstate') or not kwtools['inc']
+        or kwtools['hgcmd'] in nokwcommands.split()
+        or '.hg' in util.splitpath(repo.root)):
+        return
 
     kwtools['templater'] = kwt = kwtemplater(ui, repo)
 
--- a/hgext/mq.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/mq.py	Mon Jan 12 09:13:45 2009 +0100
@@ -963,10 +963,10 @@
                 raise
             top = self.applied[-1].name
             if ret[0]:
-                self.ui.write(
-                    "Errors during apply, please fix and refresh %s\n" % top)
+                self.ui.write(_("Errors during apply, please fix and "
+                                "refresh %s\n") % top)
             else:
-                self.ui.write("Now at: %s\n" % top)
+                self.ui.write(_("Now at: %s\n") % top)
             return ret[0]
         finally:
             del wlock
@@ -1602,7 +1602,7 @@
                 index = self.full_series_end() + i
                 self.full_series[index:index] = [patchname]
             self.parse_series()
-            self.ui.warn("adding %s to series file\n" % patchname)
+            self.ui.warn(_("adding %s to series file\n") % patchname)
             i += 1
             added.append(patchname)
             patchname = None
@@ -1786,7 +1786,7 @@
         return q.qseries(repo, start=t-1, length=1, status='A',
                          summary=opts.get('summary'))
     else:
-        ui.write("No patches applied\n")
+        ui.write(_("No patches applied\n"))
         return 1
 
 def next(ui, repo, **opts):
@@ -1794,7 +1794,7 @@
     q = repo.mq
     end = q.series_end()
     if end == len(q.series):
-        ui.write("All patches applied\n")
+        ui.write(_("All patches applied\n"))
         return 1
     return q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
 
@@ -1803,10 +1803,10 @@
     q = repo.mq
     l = len(q.applied)
     if l == 1:
-        ui.write("Only one patch applied\n")
+        ui.write(_("Only one patch applied\n"))
         return 1
     if not l:
-        ui.write("No patches applied\n")
+        ui.write(_("No patches applied\n"))
         return 1
     return q.qseries(repo, start=l-2, length=1, status='A',
                      summary=opts.get('summary'))
@@ -2018,7 +2018,7 @@
         status(q.series.index(q.lookup(patch)))
 
 def header(ui, repo, patch=None):
-    """Print the header of the topmost or specified patch"""
+    """print the header of the topmost or specified patch"""
     q = repo.mq
 
     if patch:
--- a/hgext/patchbomb.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/patchbomb.py	Mon Jan 12 09:13:45 2009 +0100
@@ -9,8 +9,7 @@
 
   The remainder of the changeset description.
 
-  [Optional] If the diffstat program is installed, the result of
-  running diffstat on the patch.
+  [Optional] The result of running diffstat on the patch.
 
   The patch itself, as generated by "hg export".
 
@@ -64,23 +63,10 @@
 import os, errno, socket, tempfile, cStringIO
 import email.MIMEMultipart, email.MIMEBase
 import email.Utils, email.Encoders, email.Generator
-from mercurial import cmdutil, commands, hg, mail, patch, util
+from mercurial import cmdutil, commands, hg, mail, mdiff, patch, util
 from mercurial.i18n import _
 from mercurial.node import bin
 
-class exportee:
-    def __init__(self, container):
-        self.lines = []
-        self.container = container
-        self.name = 'email'
-
-    def write(self, data):
-        self.lines.append(data)
-
-    def close(self):
-        self.container.append(''.join(self.lines).split('\n'))
-        self.lines = []
-
 def prompt(ui, prompt, default=None, rest=': ', empty_ok=False):
     if not ui.interactive:
         return default
@@ -99,16 +85,12 @@
 
 def cdiffstat(ui, summary, patchlines):
     s = patch.diffstat(patchlines)
-    if s:
-        if summary:
-            ui.write(summary, '\n')
-            ui.write(s, '\n')
-        ans = prompt(ui, _('Does the diffstat above look okay? '), 'y')
-        if not ans.lower().startswith('y'):
-            raise util.Abort(_('diffstat rejected'))
-    elif s is None:
-        ui.warn(_('no diffstat information available\n'))
-        s = ''
+    if summary:
+        ui.write(summary, '\n')
+        ui.write(s, '\n')
+    ans = prompt(ui, _('does the diffstat above look okay? '), 'y')
+    if not ans.lower().startswith('y'):
+        raise util.Abort(_('diffstat rejected'))
     return s
 
 def makepatch(ui, repo, patch, opts, _charsets, idx, total, patchname=None):
@@ -239,6 +221,13 @@
         o = repo.changelog.nodesbetween(o, revs or None)[0]
         return [str(repo.changelog.rev(r)) for r in o]
 
+    def getpatches(revs):
+        for r in cmdutil.revrange(repo, revs):
+            output = cStringIO.StringIO()
+            p = patch.export(repo, [r], fp=output,
+                             opts=mdiff.diffopts(git=opts.get('git')))
+            yield output.getvalue().split('\n')
+
     def getbundle(dest):
         tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
         tmpfn = os.path.join(tmpdir, 'bundle')
@@ -308,7 +297,7 @@
                  % len(patches))
 
         name = None
-        for p, i in zip(patches, xrange(len(patches))):
+        for i, p in enumerate(patches):
             jumbo.extend(p)
             if patchnames:
                 name = patchnames[i]
@@ -360,18 +349,14 @@
               ui.config('patchbomb', 'from') or
               prompt(ui, 'From', ui.username()))
 
+    # internal option used by pbranches
     patches = opts.get('patches')
     if patches:
         msgs = getpatchmsgs(patches, opts.get('patchnames'))
     elif opts.get('bundle'):
         msgs = getbundlemsgs(getbundle(dest))
     else:
-        patches = []
-        commands.export(ui, repo, *revs, **{'output': exportee(patches),
-                                            'switch_parent': False,
-                                            'text': None,
-                                            'git': opts.get('git')})
-        msgs = getpatchmsgs(patches)
+        msgs = getpatchmsgs(list(getpatches(revs)))
 
     def getaddrs(opt, prpt, default = None):
         addrs = opts.get(opt) or (ui.config('email', opt) or
--- a/hgext/purge.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/purge.py	Mon Jan 12 09:13:45 2009 +0100
@@ -32,28 +32,26 @@
 import os
 
 def purge(ui, repo, *dirs, **opts):
-    '''removes files not tracked by mercurial
+    '''removes files not tracked by Mercurial
 
-    Delete files not known to mercurial, this is useful to test local and
-    uncommitted changes in the otherwise clean source tree.
+    Delete files not known to Mercurial. This is useful to test local and
+    uncommitted changes in an otherwise-clean source tree.
 
     This means that purge will delete:
      - Unknown files: files marked with "?" by "hg status"
-     - Ignored files: files usually ignored by Mercurial because they match
-       a pattern in a ".hgignore" file
      - Empty directories: in fact Mercurial ignores directories unless they
        contain files under source control managment
     But it will leave untouched:
-     - Unmodified tracked files
-     - Modified tracked files
+     - Modified and unmodified tracked files
+     - Ignored files (unless --all is specified)
      - New files added to the repository (with "hg add")
 
     If directories are given on the command line, only files in these
     directories are considered.
 
-    Be careful with purge, you could irreversibly delete some files you
+    Be careful with purge, as you could irreversibly delete some files you
     forgot to add to the repository. If you only want to print the list of
-    files that this program would delete use the --print option.
+    files that this program would delete, use the --print option.
     '''
     act = not opts['print']
     eol = '\n'
@@ -64,7 +62,7 @@
     def remove(remove_func, name):
         if act:
             try:
-                remove_func(os.path.join(repo.root, name))
+                remove_func(repo.wjoin(name))
             except OSError:
                 m = _('%s cannot be removed') % name
                 if opts['abort_on_err']:
--- a/hgext/rebase.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/rebase.py	Mon Jan 12 09:13:45 2009 +0100
@@ -34,7 +34,7 @@
     if not first:
         ancestor.ancestor = newancestor
     else:
-        repo.ui.debug(_("First revision, do not change ancestor\n"))
+        repo.ui.debug(_("first revision, do not change ancestor\n"))
     stats = merge.update(repo, rev, True, True, False)
     return stats
 
--- a/hgext/win32mbcs.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/win32mbcs.py	Mon Jan 12 09:13:45 2009 +0100
@@ -8,7 +8,7 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 #
-"""Allow to use MBCS path with problematic encoding.
+"""allow to use MBCS path with problematic encoding.
 
 Some MBCS encodings are not good for some path operations
 (i.e. splitting path, case conversion, etc.) with its encoded bytes.
--- a/hgext/zeroconf/__init__.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/hgext/zeroconf/__init__.py	Mon Jan 12 09:13:45 2009 +0100
@@ -6,6 +6,29 @@
 # the GNU General Public License (version 2), incorporated herein by
 # reference.
 
+'''zeroconf support for mercurial repositories
+
+Zeroconf enabled repositories will be announced in a network without the need
+to configure a server or a service. They can be discovered without knowing
+their actual IP address.
+
+To use the zeroconf extension add the following entry to your hgrc file:
+
+[extensions]
+hgext.zeroconf =
+
+To allow other people to discover your repository using run "hg serve" in your
+repository.
+
+ $ cd test
+ $ hg serve
+
+You can discover zeroconf enabled repositories by running "hg paths".
+
+ $ hg paths
+ zc-test = http://example.com:8000/test
+'''
+
 import Zeroconf, socket, time, os
 from mercurial import ui
 from mercurial import extensions
--- a/mercurial/changelog.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/changelog.py	Mon Jan 12 09:13:45 2009 +0100
@@ -179,7 +179,7 @@
 
         user = user.strip()
         if "\n" in user:
-            raise RevlogError(_("username %s contains a newline") % `user`)
+            raise RevlogError(_("username %s contains a newline") % repr(user))
         user, desc = util.fromlocal(user), util.fromlocal(desc)
 
         if date:
--- a/mercurial/commands.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/commands.py	Mon Jan 12 09:13:45 2009 +0100
@@ -328,29 +328,34 @@
     state = hbisect.load_state(repo)
 
     if command:
+        commandpath = util.find_exe(command)
         changesets = 1
-        while changesets:
-            # update state
-            status = os.spawnlp(os.P_WAIT, command)
-            node = repo.lookup(rev or '.')
-            if status == 125:
-                transition = "skip"
-            elif status == 0:
-                transition = "good"
-            # status < 0 means process was killed
-            elif status == 127 or status < 0:
-                break
-            else:
-                transition = "bad"
-            state[transition].append(node)
-            ui.note(_('Changeset %s: %s\n') % (short(node), transition))
-            check_state(state, interactive=False)
-            # bisect
-            nodes, changesets, good = hbisect.bisect(repo.changelog, state)
-            # update to next check
-            cmdutil.bail_if_changed(repo)
-            hg.clean(repo, nodes[0], show_stats=False)
-        hbisect.save_state(repo, state)
+        try:
+            while changesets:
+                # update state
+                status = os.spawnl(os.P_WAIT, commandpath)
+                if status == 125:
+                    transition = "skip"
+                elif status == 0:
+                    transition = "good"
+                # status < 0 means process was killed
+                elif status == 127:
+                    raise util.Abort(_("failed to execute %s") % command)
+                elif status < 0:
+                    raise util.Abort(_("%s killed") % command)
+                else:
+                    transition = "bad"
+                node = repo.lookup(rev or '.')
+                state[transition].append(node)
+                ui.note(_('Changeset %s: %s\n') % (short(node), transition))
+                check_state(state, interactive=False)
+                # bisect
+                nodes, changesets, good = hbisect.bisect(repo.changelog, state)
+                # update to next check
+                cmdutil.bail_if_changed(repo)
+                hg.clean(repo, nodes[0], show_stats=False)
+        finally:
+            hbisect.save_state(repo, state)
         return print_result(nodes, not status)
 
     # update state
@@ -688,7 +693,10 @@
         ui.write("%s\n" % "\n".join(options))
         return
 
-    ui.write("%s\n" % "\n".join(util.sort(cmdutil.findpossible(cmd, table))))
+    cmdlist = cmdutil.findpossible(cmd, table)
+    if ui.verbose:
+        cmdlist = [' '.join(c[0]) for c in cmdlist.values()]
+    ui.write("%s\n" % "\n".join(util.sort(cmdlist)))
 
 def debugfsinfo(ui, path = "."):
     file('.debugfsinfo', 'w').write('')
@@ -1325,7 +1333,7 @@
         # description
         doc = gettext(i[0].__doc__)
         if not doc:
-            doc = _("(No help text available)")
+            doc = _("(no help text available)")
         if ui.quiet:
             doc = doc.splitlines(0)[0]
         ui.write("\n%s\n" % doc.rstrip())
@@ -1354,7 +1362,7 @@
                 continue
             doc = gettext(e[0].__doc__)
             if not doc:
-                doc = _("(No help text available)")
+                doc = _("(no help text available)")
             h[f] = doc.splitlines(0)[0].rstrip()
             cmds[f] = c.lstrip("^")
 
@@ -1397,7 +1405,7 @@
 
         # description
         if not doc:
-            doc = _("(No help text available)")
+            doc = _("(no help text available)")
         if callable(doc):
             doc = doc()
 
@@ -1410,7 +1418,7 @@
         except KeyError:
             raise cmdutil.UnknownCommand(name)
 
-        doc = gettext(mod.__doc__) or _('No help text available')
+        doc = gettext(mod.__doc__) or _('no help text available')
         doc = doc.splitlines(0)
         ui.write(_('%s extension - %s\n') % (name.split('.')[-1], doc[0]))
         for d in doc[1:]:
@@ -1794,7 +1802,7 @@
         if not rev and abs not in repo.dirstate:
             continue
         if opts.get('fullpath'):
-            ui.write(os.path.join(repo.root, abs), end)
+            ui.write(repo.wjoin(abs), end)
         else:
             ui.write(((pats and m.rel(abs)) or abs), end)
         ret = 0
@@ -2822,8 +2830,6 @@
 def tags(ui, repo):
     """list repository tags
 
-    List the repository tags.
-
     This lists both regular and local tags. When the -v/--verbose switch
     is used, a third column "local" is printed for local tags.
     """
@@ -3071,7 +3077,7 @@
           ('g', 'good', False, _('mark changeset good')),
           ('b', 'bad', False, _('mark changeset bad')),
           ('s', 'skip', False, _('skip testing changeset')),
-          ('c', 'command', '', _('Use command to check changeset state')),
+          ('c', 'command', '', _('use command to check changeset state')),
           ('U', 'noupdate', False, _('do not update to target'))],
          _("[-gbsr] [-c CMD] [REV]")),
     "branch":
--- a/mercurial/dispatch.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/dispatch.py	Mon Jan 12 09:13:45 2009 +0100
@@ -90,7 +90,7 @@
             else:
                 raise
     except socket.error, inst:
-        ui.warn(_("abort: %s\n") % inst[-1])
+        ui.warn(_("abort: %s\n") % inst.args[-1])
     except IOError, inst:
         if hasattr(inst, "code"):
             ui.warn(_("abort: %s\n") % inst)
@@ -100,7 +100,7 @@
             except: # it might be anything, for example a string
                 reason = inst.reason
             ui.warn(_("abort: error: %s\n") % reason)
-        elif hasattr(inst, "args") and inst[0] == errno.EPIPE:
+        elif hasattr(inst, "args") and inst.args[0] == errno.EPIPE:
             if ui.debugflag:
                 ui.warn(_("broken pipe\n"))
         elif getattr(inst, "strerror", None):
@@ -116,13 +116,13 @@
         else:
             ui.warn(_("abort: %s\n") % inst.strerror)
     except util.UnexpectedOutput, inst:
-        ui.warn(_("abort: %s") % inst[0])
-        if not isinstance(inst[1], basestring):
-            ui.warn(" %r\n" % (inst[1],))
-        elif not inst[1]:
+        ui.warn(_("abort: %s") % inst.args[0])
+        if not isinstance(inst.args[1], basestring):
+            ui.warn(" %r\n" % (inst.args[1],))
+        elif not inst.args[1]:
             ui.warn(_(" empty string\n"))
         else:
-            ui.warn("\n%r\n" % util.ellipsis(inst[1]))
+            ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
     except ImportError, inst:
         m = str(inst).split()[-1]
         ui.warn(_("abort: could not import module %s!\n") % m)
--- a/mercurial/hbisect.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/hbisect.py	Mon Jan 12 09:13:45 2009 +0100
@@ -102,7 +102,7 @@
             if value == perfect: # found a perfect candidate? quit early
                 break
 
-        if y < perfect: # all downhill from here?
+        if y < perfect and rev not in skip: # all downhill from here?
             for c in children.get(rev, []):
                 poison[c] = True # poison children
             continue
--- a/mercurial/hg.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/hg.py	Mon Jan 12 09:13:45 2009 +0100
@@ -252,8 +252,6 @@
     note = ", ".join([_("%d files %s") % s for s in stats])
     repo.ui.status("%s\n" % note)
 
-def _update(repo, node): return update(repo, node)
-
 def update(repo, node):
     """update the working directory to node, merging linear changes"""
     stats = _merge.update(repo, node, False, False, None)
@@ -262,6 +260,9 @@
         repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
     return stats[3] > 0
 
+# naming conflict in clone()
+_update = update
+
 def clean(repo, node, show_stats=True):
     """forcibly switch the working directory to node, clobbering changes"""
     stats = _merge.update(repo, node, False, True, None)
--- a/mercurial/hgweb/hgweb_mod.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/hgweb/hgweb_mod.py	Mon Jan 12 09:13:45 2009 +0100
@@ -284,14 +284,13 @@
             raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
 
         allow_read = self.configlist('web', 'allow_read')
-        result = (not allow_read) or (allow_read == ['*']) or (user in allow_read)
-        if not result:
+        result = (not allow_read) or (allow_read == ['*'])
+        if not result or user in allow_read:
             raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
 
         if op == 'pull' and not self.allowpull:
             raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
-        # op is None when checking allow/deny_read permissions for a web-browser request
-        elif op == 'pull' or op is None:
+        elif op == 'pull' or op is None: # op is None for interface requests
             return
 
         # enforce that you can only push using POST requests
--- a/mercurial/hgweb/hgwebdir_mod.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/hgweb/hgwebdir_mod.py	Mon Jan 12 09:13:45 2009 +0100
@@ -102,11 +102,11 @@
 
         user = req.env.get('REMOTE_USER')
 
-        deny_read = ui.configlist('web', 'deny_read', default=None, untrusted=True)
+        deny_read = ui.configlist('web', 'deny_read', untrusted=True)
         if deny_read and (not user or deny_read == ['*'] or user in deny_read):
             return False
 
-        allow_read = ui.configlist('web', 'allow_read', default=None, untrusted=True)
+        allow_read = ui.configlist('web', 'allow_read', untrusted=True)
         # by default, allow reading if no allow_read option has been set
         if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
             return True
--- a/mercurial/localrepo.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/localrepo.py	Mon Jan 12 09:13:45 2009 +0100
@@ -1579,7 +1579,7 @@
         if self.ui.verbose or source == 'bundle':
             self.ui.status(_("%d changesets found\n") % len(nodes))
         if self.ui.debugflag:
-            self.ui.debug(_("List of changesets:\n"))
+            self.ui.debug(_("list of changesets:\n"))
             for node in nodes:
                 self.ui.debug("%s\n" % hex(node))
 
--- a/mercurial/patch.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/patch.py	Mon Jan 12 09:13:45 2009 +0100
@@ -9,7 +9,7 @@
 from i18n import _
 from node import hex, nullid, short
 import base85, cmdutil, mdiff, util, revlog, diffhelpers, copies
-import cStringIO, email.Parser, os, re, errno
+import cStringIO, email.Parser, os, re, errno, math
 import sys, tempfile, zlib
 
 gitre = re.compile('diff --git a/(.*) b/(.*)')
@@ -794,9 +794,7 @@
 
     def readline(self):
         if self.buf:
-            l = self.buf[0]
-            del self.buf[0]
-            return l
+            return self.buf.pop(0)
         return self.fp.readline()
 
     def __iter__(self):
@@ -1059,7 +1057,7 @@
         gp = patches[f]
         if gp and gp.mode:
             islink, isexec = gp.mode
-            dst = os.path.join(repo.root, gp.path)
+            dst = repo.wjoin(gp.path)
             # patch won't create empty files
             if gp.op == 'ADD' and not os.path.exists(dst):
                 flags = (isexec and 'x' or '') + (islink and 'l' or '')
@@ -1340,19 +1338,61 @@
 
         for chunk in diff(repo, prev, node, opts=opts):
             fp.write(chunk)
-        if fp not in (sys.stdout, repo.ui):
-            fp.close()
 
     for seqno, rev in enumerate(revs):
         single(rev, seqno+1, fp)
 
-def diffstat(patchlines):
-    if not util.find_exe('diffstat'):
-        return
-    output = util.filter('\n'.join(patchlines),
-                         'diffstat -p1 -w79 2>%s' % util.nulldev)
-    stat = [l.lstrip() for l in output.splitlines(True)]
-    last = stat.pop()
-    stat.insert(0, last)
-    stat = ''.join(stat)
-    return stat
+def diffstatdata(lines):
+    filename = None
+    for line in lines:
+        if line.startswith('diff'):
+            if filename:
+                yield (filename, adds, removes)
+            # set numbers to 0 anyway when starting new file
+            adds = 0
+            removes = 0
+            if line.startswith('diff --git'):
+                filename = gitre.search(line).group(1)
+            else:
+                # format: "diff -r ... -r ... file name"
+                filename = line.split(None, 5)[-1]
+        elif line.startswith('+') and not line.startswith('+++'):
+            adds += 1
+        elif line.startswith('-') and not line.startswith('---'):
+            removes += 1
+    yield (filename, adds, removes)
+
+def diffstat(lines):
+    output = []
+    stats = list(diffstatdata(lines))
+    width = util.termwidth() - 2
+
+    maxtotal, maxname = 0, 0
+    totaladds, totalremoves = 0, 0
+    for filename, adds, removes in stats:
+        totaladds += adds
+        totalremoves += removes
+        maxname = max(maxname, len(filename))
+        maxtotal = max(maxtotal, adds+removes)
+
+    countwidth = len(str(maxtotal))
+    graphwidth = width - countwidth - maxname
+    if graphwidth < 10:
+        graphwidth = 10
+
+    factor = int(math.ceil(float(maxtotal) / graphwidth))
+
+    for filename, adds, removes in stats:
+        # If diffstat runs out of room it doesn't print anything, which
+        # isn't very useful, so always print at least one + or - if there
+        # were at least some changes
+        pluses = '+' * max(adds/factor, int(bool(adds)))
+        minuses = '-' * max(removes/factor, int(bool(removes)))
+        output.append(' %-*s |  %*.d %s%s\n' % (maxname, filename, countwidth,
+                                                adds+removes, pluses, minuses))
+
+    if stats:
+        output.append(' %d files changed, %d insertions(+), %d deletions(-)\n' %
+                      (len(stats), totaladds, totalremoves))
+
+    return ''.join(output)
--- a/mercurial/ui.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/ui.py	Mon Jan 12 09:13:45 2009 +0100
@@ -340,7 +340,7 @@
         if user is None:
             user = os.environ.get("EMAIL")
         if user is None and self.configbool("ui", "askusername"):
-            user = self.prompt(_("Enter a commit username:"), default=None)
+            user = self.prompt(_("enter a commit username:"), default=None)
         if user is None:
             try:
                 user = '%s@%s' % (util.getuser(), socket.getfqdn())
@@ -350,7 +350,7 @@
         if not user:
             raise util.Abort(_("Please specify a username."))
         if "\n" in user:
-            raise util.Abort(_("username %s contains a newline\n") % `user`)
+            raise util.Abort(_("username %s contains a newline\n") % repr(user))
         return user
 
     def shortuser(self, user):
--- a/mercurial/util.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/util.py	Mon Jan 12 09:13:45 2009 +0100
@@ -705,7 +705,7 @@
         if cwd is not None and oldcwd != cwd:
             os.chdir(oldcwd)
 
-class SignatureError:
+class SignatureError(Exception):
     pass
 
 def checksignature(func):
@@ -1991,3 +1991,24 @@
 def uirepr(s):
     # Avoid double backslash in Windows path repr()
     return repr(s).replace('\\\\', '\\')
+
+def termwidth():
+    if 'COLUMNS' in os.environ:
+        try:
+            return int(os.environ['COLUMNS'])
+        except ValueError:
+            pass
+    try:
+        import termios, array, fcntl
+        for dev in (sys.stdout, sys.stdin):
+            try:
+                fd = dev.fileno()
+                if not os.isatty(fd):
+                    continue
+                arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
+                return array.array('h', arri)[1]
+            except ValueError:
+                pass
+    except ImportError:
+        pass
+    return 80
--- a/mercurial/util_win32.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/mercurial/util_win32.py	Mon Jan 12 09:13:45 2009 +0100
@@ -19,7 +19,7 @@
 import util
 from win32com.shell import shell,shellcon
 
-class WinError:
+class WinError(Exception):
     winerror_map = {
         winerror.ERROR_ACCESS_DENIED: errno.EACCES,
         winerror.ERROR_ACCOUNT_DISABLED: errno.EACCES,
--- a/setup.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/setup.py	Mon Jan 12 09:13:45 2009 +0100
@@ -9,6 +9,23 @@
 if not hasattr(sys, 'version_info') or sys.version_info < (2, 3, 0, 'final'):
     raise SystemExit("Mercurial requires python 2.3 or later.")
 
+# Solaris Python packaging brain damage
+try:
+    import hashlib
+    sha = hashlib.sha1()
+except:
+    try:
+        import sha
+    except:
+        raise SystemExit(
+            "Couldn't import standard hashlib (incomplete Python install).")
+
+try:
+    import zlib
+except:
+    raise SystemExit(
+        "Couldn't import standard zlib (incomplete Python install).")
+
 import os
 import shutil
 import tempfile
Binary file tests/tampered.hg has changed
--- a/tests/test-acl.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-acl.out	Mon Jan 12 09:13:45 2009 +0100
@@ -15,7 +15,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -43,7 +43,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -75,7 +75,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -112,7 +112,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -151,7 +151,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -193,7 +193,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -234,7 +234,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -278,7 +278,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -321,7 +321,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -365,7 +365,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -409,7 +409,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -458,7 +458,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -506,7 +506,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
@@ -561,7 +561,7 @@
 searching for changes
 common changesets up to 6675d58eff77
 3 changesets found
-List of changesets:
+list of changesets:
 ef1ea85a6374b77d6da9dcda9541f498f2d17df7
 f9cafe1212c8c6fa1120d14a556e18cc44ff8bdd
 911600dab2ae7a9baff75958b84fe606851ce955
--- a/tests/test-audit-path	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-audit-path	Mon Jan 12 09:13:45 2009 +0100
@@ -41,4 +41,8 @@
 hg manifest -r3
 hg update -Cr3
 
+echo % attack /tmp/test
+hg manifest -r4
+hg update -Cr4 2>&1 | sed -e "s|$HGTMP|[HGTMP]|"
+
 exit 0
--- a/tests/test-audit-path.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-audit-path.out	Mon Jan 12 09:13:45 2009 +0100
@@ -10,7 +10,7 @@
 adding changesets
 adding manifests
 adding file changes
-added 4 changesets with 5 changes to 5 files (+3 heads)
+added 5 changesets with 6 changes to 6 files (+4 heads)
 (run 'hg heads' to see heads, 'hg merge' to merge)
 % attack .hg/test
 .hg/test
@@ -25,3 +25,6 @@
 % attack ../test
 ../test
 abort: path contains illegal component: ../test
+% attack /tmp/test
+/tmp/test
+abort: No such file or directory: [HGTMP]/test-audit-path/target//tmp/test
--- a/tests/test-bdiff	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-bdiff	Mon Jan 12 09:13:45 2009 +0100
@@ -9,13 +9,13 @@
     if d:
         c = mpatch.patches(a, [d])
     if c != b:
-        print "***", `a`, `b`
+        print "***", repr(a), repr(b)
         print "bad:"
-        print `c`[:200]
-        print `d`
+        print repr(c)[:200]
+        print repr(d)
 
 def test(a, b):
-    print "***", `a`, `b`
+    print "***", repr(a), repr(b)
     test1(a, b)
     test1(b, a)
 
--- a/tests/test-bisect	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-bisect	Mon Jan 12 09:13:45 2009 +0100
@@ -72,3 +72,13 @@
 echo % test no action
 hg bisect -r
 hg bisect || echo failure
+
+echo % reproduce AssertionError, issue1445
+hg bisect -r
+hg bisect -b 6
+hg bisect -g 0
+hg bisect -s
+hg bisect -s
+hg bisect -s
+hg bisect -s 
+hg bisect -g
--- a/tests/test-bisect.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-bisect.out	Mon Jan 12 09:13:45 2009 +0100
@@ -286,3 +286,20 @@
 % test no action
 abort: cannot bisect (no known good revisions)
 failure
+% reproduce AssertionError, issue1445
+Testing changeset 3:b53bea5e2fcb (6 changesets remaining, ~2 tests)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+Testing changeset 2:db07c04beaca (6 changesets remaining, ~2 tests)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+Testing changeset 4:9b2ba8336a65 (6 changesets remaining, ~2 tests)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+Testing changeset 1:5cd978ea5149 (6 changesets remaining, ~2 tests)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+Testing changeset 5:7874a09ea728 (6 changesets remaining, ~2 tests)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+The first bad revision is:
+changeset:   6:a3d5c6fdf0d3
+user:        test
+date:        Thu Jan 01 00:00:06 1970 +0000
+summary:     msg 6
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-bookmarks-current	Mon Jan 12 09:13:45 2009 +0100
@@ -0,0 +1,41 @@
+#!/bin/sh
+
+echo "[extensions]" >> $HGRCPATH
+echo "bookmarks=" >> $HGRCPATH
+
+echo "[bookmarks]" >> $HGRCPATH
+echo "track.current = True" >> $HGRCPATH
+
+hg init
+
+echo % no bookmarks
+hg bookmarks
+
+echo % set bookmark X
+hg bookmark X
+
+echo % update to bookmark X
+hg update X
+
+echo % list bookmarks
+hg bookmarks
+
+echo % rename
+hg bookmark -m X Z
+
+echo % list bookmarks
+hg bookmarks
+
+echo % new bookmark Y
+hg bookmark Y
+
+echo % list bookmarks
+hg bookmark
+
+echo % commit
+echo 'b' > b
+hg add b
+hg commit -m'test'
+
+echo % list bookmarks
+hg bookmark
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-bookmarks-current.out	Mon Jan 12 09:13:45 2009 +0100
@@ -0,0 +1,18 @@
+% no bookmarks
+no bookmarks set
+% set bookmark X
+% update to bookmark X
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% list bookmarks
+ * X                         -1:000000000000
+% rename
+% list bookmarks
+ * Z                         -1:000000000000
+% new bookmark Y
+% list bookmarks
+   Y                         -1:000000000000
+ * Z                         -1:000000000000
+% commit
+% list bookmarks
+   Y                         -1:000000000000
+ * Z                         0:719295282060
--- a/tests/test-bookmarks-rebase.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-bookmarks-rebase.out	Mon Jan 12 09:13:45 2009 +0100
@@ -18,6 +18,8 @@
 rebase completed
 changeset:   3:9163974d1cb5
 tag:         tip
+tag:         two
+tag:         one
 parent:      1:925d80f479bb
 parent:      2:db815d6d32e6
 user:        test
--- a/tests/test-bookmarks.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-bookmarks.out	Mon Jan 12 09:13:45 2009 +0100
@@ -7,6 +7,7 @@
  * X                         0:f7b1eb17ad24
 % look up bookmark
 changeset:   0:f7b1eb17ad24
+tag:         X
 tag:         tip
 user:        test
 date:        Thu Jan 01 00:00:00 1970 +0000
@@ -51,7 +52,10 @@
  * x  y                      2:0316ce92851d
 % look up stripped bookmark name
 changeset:   2:0316ce92851d
+tag:         X2
+tag:         Y
 tag:         tip
+tag:         x  y
 user:        test
 date:        Thu Jan 01 00:00:00 1970 +0000
 summary:     2
--- a/tests/test-convert-bzr-merges	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-convert-bzr-merges	Mon Jan 12 09:13:45 2009 +0100
@@ -18,6 +18,7 @@
 bzr add -q file-branch1
 bzr commit -q -m 'Added branch1 file'
 cd ../source
+sleep 1
 echo content > file-parent
 bzr add -q file-parent
 bzr commit -q -m 'Added parent file'
@@ -27,6 +28,7 @@
 echo somecontent > file-branch2
 bzr add -q file-branch2
 bzr commit -q -m 'Added brach2 file'
+sleep 1
 cd ../source
 bzr merge -q ../source-branch1
 bzr merge -q --force ../source-branch2
--- a/tests/test-convert-cvs-builtincvsps	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-convert-cvs-builtincvsps	Mon Jan 12 09:13:45 2009 +0100
@@ -100,5 +100,22 @@
 hgcat b/c
 hg -R src-filemap log --template '#rev# #desc# files: #files#\n'
 
+echo % commit a new revision with funny log message
+cd src
+sleep 1
+echo e >> a
+cvscall -q commit -m'funny
+----------------------------
+log message' . | grep '<--' |\
+    sed -e 's:.*src/\(.*\),v.*:checking in src/\1,v:g'
+cd ..
+
+echo % convert again
+hg convert src src-hg | sed -e 's/connecting to.*cvsrepo/connecting to cvsrepo/g'
+
 echo "graphlog = " >> $HGRCPATH
 hg -R src-hg glog --template '#rev# (#branches#) #desc# files: #files#\n'
+
+echo % testing debugcvsps
+cd src
+hg debugcvsps | sed -e 's/Author:.*/Author:/' -e 's/Date:.*/Date:/' 
--- a/tests/test-convert-cvs-builtincvsps.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-convert-cvs-builtincvsps.out	Mon Jan 12 09:13:45 2009 +0100
@@ -124,6 +124,22 @@
 2 update tags files: .hgtags
 1 ci0 files: b/c
 0 Initial revision files: b/c
+% commit a new revision with funny log message
+checking in src/a,v
+% convert again
+using builtin cvsps
+collecting CVS rlog
+9 log entries
+creating changesets
+6 changeset entries
+connecting to cvsrepo
+scanning source...
+sorting...
+converting...
+0 funny
+o  6 (branch) funny
+|  ----------------------------
+|  log message files: a
 o  5 (branch) ci2 files: b/c
 |
 | o  4 () ci1 files: a b/c
@@ -136,3 +152,85 @@
 |/
 o  0 () Initial revision files: a b/c
 
+% testing debugcvsps
+collecting CVS rlog
+9 log entries
+creating changesets
+6 changeset entries
+---------------------
+PatchSet 1 
+Date:
+Author:
+Branch: HEAD
+Tag: (none) 
+Log:
+Initial revision
+
+Members: 
+	a:INITIAL->1.1 
+	b/c:INITIAL->1.1 
+
+---------------------
+PatchSet 2 
+Date:
+Author:
+Branch: INITIAL
+Tag: start 
+Log:
+import
+
+Members: 
+	a:1.1->1.1.1.1 
+	b/c:1.1->1.1.1.1 
+
+---------------------
+PatchSet 3 
+Date:
+Author:
+Branch: HEAD
+Tag: (none) 
+Log:
+ci0
+
+Members: 
+	b/c:1.1->1.2 
+
+---------------------
+PatchSet 4 
+Date:
+Author:
+Branch: HEAD
+Tag: (none) 
+Log:
+ci1
+
+Members: 
+	a:1.1->1.2 
+	b/c:1.2->1.3 
+
+---------------------
+PatchSet 5 
+Date:
+Author:
+Branch: branch
+Tag: (none) 
+Log:
+ci2
+
+Members: 
+	b/c:1.1->1.1.2.1 
+
+---------------------
+PatchSet 6 
+Date:
+Author:
+Branch: branch
+Tag: (none) 
+Log:
+funny
+----------------------------
+log message
+
+Members: 
+	a:1.2->1.2.2.1 
+
--- a/tests/test-convert.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-convert.out	Mon Jan 12 09:13:45 2009 +0100
@@ -1,6 +1,6 @@
 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
 
-Convert a foreign SCM repository to a Mercurial one.
+convert a foreign SCM repository to a Mercurial one.
 
     Accepted source formats [identifiers]:
     - Mercurial [hg]
--- a/tests/test-mq.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-mq.out	Mon Jan 12 09:13:45 2009 +0100
@@ -33,7 +33,7 @@
  qfold        fold the named patches into the current patch
  qgoto        push or pop patches until named patch is at top of stack
  qguard       set or print guards for a patch
- qheader      Print the header of the topmost or specified patch
+ qheader      print the header of the topmost or specified patch
  qimport      import a patch
  qinit        init a new queue repository
  qnew         create a new patch
--- a/tests/test-notify.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-notify.out	Mon Jan 12 09:13:45 2009 +0100
@@ -150,7 +150,8 @@
 	b
 diffstat:
 
-files patched: 1
+ a |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 diffs (6 lines):
 
--- a/tests/test-patchbomb	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-patchbomb	Mon Jan 12 09:13:45 2009 +0100
@@ -11,6 +11,8 @@
 echo "[extensions]" >> $HGRCPATH
 echo "patchbomb=" >> $HGRCPATH
 
+COLUMNS=80; export COLUMNS
+
 hg init t
 cd t
 echo a > a
--- a/tests/test-patchbomb.out	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-patchbomb.out	Mon Jan 12 09:13:45 2009 +0100
@@ -196,7 +196,8 @@
 
 c
 
-files patched: 1
+ c |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 
 Displaying [PATCH] test ...
@@ -211,7 +212,8 @@
 To: foo
 Cc: bar
 
-files patched: 1
+ c |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 
 # HG changeset patch
@@ -232,15 +234,19 @@
 
 a
 
-files patched: 1
+ a |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 b
 
-files patched: 1
+ b |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 Final summary:
 
-files patched: 2
+ a |  1 +
+ b |  1 +
+ 2 files changed, 2 insertions(+), 0 deletions(-)
 
 
 Write the introductory message for the patch series.
@@ -258,7 +264,9 @@
 Cc: bar
 
 
-files patched: 2
+ a |  1 +
+ b |  1 +
+ 2 files changed, 2 insertions(+), 0 deletions(-)
 
 Displaying [PATCH 1 of 2] a ...
 Content-Type: text/plain; charset="us-ascii"
@@ -274,7 +282,8 @@
 To: foo
 Cc: bar
 
-files patched: 1
+ a |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 
 # HG changeset patch
@@ -304,7 +313,8 @@
 To: foo
 Cc: bar
 
-files patched: 1
+ b |  1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
 
 
 # HG changeset patch
--- a/tests/test-pull-http	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-pull-http	Mon Jan 12 09:13:45 2009 +0100
@@ -19,7 +19,7 @@
 hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
 cat hg.pid >> $DAEMON_PIDS
 hg clone http://localhost:$HGPORT/ test3 | sed -e 's,:[0-9][0-9]*/,/,'
-kill `cat hg.pid`
+"$TESTDIR/killdaemons.py"
 echo % serve errors
 cat errors.log
 
--- a/tests/test-walkrepo.py	Mon Jan 12 09:12:35 2009 +0100
+++ b/tests/test-walkrepo.py	Mon Jan 12 09:13:45 2009 +0100
@@ -24,26 +24,26 @@
     reposet = frozenset(walkrepos('.', followsym=True))
     if sym and (len(reposet) != 3):
         print "reposet = %r" % (reposet,)
-        raise SystemExit(1, "Found %d repositories when I should have found 3" % (len(reposet),))
+        print "Found %d repositories when I should have found 3" % (len(reposet),)
     if (not sym) and (len(reposet) != 2):
         print "reposet = %r" % (reposet,)
-        raise SystemExit(1, "Found %d repositories when I should have found 2" % (len(reposet),))
+        print "Found %d repositories when I should have found 2" % (len(reposet),)
     sub1set = frozenset((pjoin('.', 'sub1'),
                          pjoin('.', 'circle', 'subdir', 'sub1')))
     if len(sub1set & reposet) != 1:
         print "sub1set = %r" % (sub1set,)
         print "reposet = %r" % (reposet,)
-        raise SystemExit(1, "sub1set and reposet should have exactly one path in common.")
+        print "sub1set and reposet should have exactly one path in common."
     sub2set = frozenset((pjoin('.', 'subsub1'),
                          pjoin('.', 'subsubdir', 'subsub1')))
     if len(sub2set & reposet) != 1:
         print "sub2set = %r" % (sub2set,)
         print "reposet = %r" % (reposet,)
-        raise SystemExit(1, "sub1set and reposet should have exactly one path in common.")
+        print "sub1set and reposet should have exactly one path in common."
     sub3 = pjoin('.', 'circle', 'top1')
     if sym and not (sub3 in reposet):
         print "reposet = %r" % (reposet,)
-        raise SystemExit(1, "Symbolic links are supported and %s is not in reposet" % (sub3,))
+        print "Symbolic links are supported and %s is not in reposet" % (sub3,)
 
 runtest()
 if sym: