merge with crew.
authorVadim Gelfer <vadim.gelfer@gmail.com>
Wed, 03 May 2006 22:47:57 -0700
changeset 2194 ee90e5a9197f
parent 2192 2be3ac7abc21 (diff)
parent 2193 fb28ce04b349 (current diff)
child 2196 2a5d8af8eecc
merge with crew.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/bugzilla.py	Wed May 03 22:47:57 2006 -0700
@@ -0,0 +1,293 @@
+# bugzilla.py - bugzilla integration for mercurial
+#
+# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+#
+# 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:
+#
+# REQUIRED:
+#   host = bugzilla # mysql server where bugzilla database lives
+#   password = **   # user's password
+#   version = 2.16  # version of bugzilla installed
+#
+# OPTIONAL:
+#   bzuser = ...    # bugzilla user id to record comments with
+#   db = bugs       # database to connect to
+#   hgweb = http:// # root of hg web site for browsing commits
+#   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
+
+from mercurial.demandload import *
+from mercurial.i18n import gettext as _
+from mercurial.node import *
+demandload(globals(), 'cStringIO mercurial:templater,util os re time')
+
+try:
+    import MySQLdb
+except ImportError:
+    raise util.Abort(_('python mysql support not available'))
+
+def buglist(ids):
+    return '(' + ','.join(map(str, ids)) + ')'
+
+class bugzilla_2_16(object):
+    '''support for bugzilla version 2.16.'''
+
+    def __init__(self, ui):
+        self.ui = ui
+        host = self.ui.config('bugzilla', 'host', 'localhost')
+        user = self.ui.config('bugzilla', 'user', 'bugs')
+        passwd = self.ui.config('bugzilla', 'password')
+        db = self.ui.config('bugzilla', 'db', 'bugs')
+        timeout = int(self.ui.config('bugzilla', 'timeout', 5))
+        self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
+                     (host, db, user, '*' * len(passwd)))
+        self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
+                                    db=db, connect_timeout=timeout)
+        self.cursor = self.conn.cursor()
+        self.run('select fieldid from fielddefs where name = "longdesc"')
+        ids = self.cursor.fetchall()
+        if len(ids) != 1:
+            raise util.Abort(_('unknown database schema'))
+        self.longdesc_id = ids[0][0]
+        self.user_ids = {}
+
+    def run(self, *args, **kwargs):
+        '''run a query.'''
+        self.ui.note(_('query: %s %s\n') % (args, kwargs))
+        try:
+            self.cursor.execute(*args, **kwargs)
+        except MySQLdb.MySQLError, err:
+            self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
+            raise
+
+    def filter_real_bug_ids(self, ids):
+        '''filter not-existing bug ids from list.'''
+        self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
+        ids = [c[0] for c in self.cursor.fetchall()]
+        ids.sort()
+        return ids
+
+    def filter_unknown_bug_ids(self, node, ids):
+        '''filter bug ids from list that already refer to this changeset.'''
+
+        self.run('''select bug_id from longdescs where
+                    bug_id in %s and thetext like "%%%s%%"''' %
+                 (buglist(ids), short(node)))
+        unknown = dict.fromkeys(ids)
+        for (id,) in self.cursor.fetchall():
+            self.ui.status(_('bug %d already knows about changeset %s\n') %
+                           (id, short(node)))
+            unknown.pop(id, None)
+        ids = unknown.keys()
+        ids.sort()
+        return ids
+
+    def notify(self, ids):
+        '''tell bugzilla to send mail.'''
+
+        self.ui.status(_('telling bugzilla to send mail:\n'))
+        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
+            fp = os.popen('(%s) 2>&1' % cmd)
+            out = fp.read()
+            ret = fp.close()
+            if ret:
+                self.ui.warn(out)
+                raise util.Abort(_('bugzilla notify command %s') %
+                                 util.explain_exit(ret)[0])
+        self.ui.status(_('done\n'))
+
+    def get_user_id(self, user):
+        '''look up numeric bugzilla user id.'''
+        try:
+            return self.user_ids[user]
+        except KeyError:
+            try:
+                userid = int(user)
+            except ValueError:
+                self.ui.note(_('looking up user %s\n') % user)
+                self.run('''select userid from profiles
+                            where login_name like %s''', user)
+                all = self.cursor.fetchall()
+                if len(all) != 1:
+                    raise KeyError(user)
+                userid = int(all[0][0])
+            self.user_ids[user] = userid
+            return userid
+
+    def add_comment(self, bugid, text, prefuser):
+        '''add comment to bug. try adding comment as committer of
+        changeset, otherwise as default bugzilla user.'''
+        try:
+            userid = self.get_user_id(prefuser)
+        except KeyError:
+            try:
+                defaultuser = self.ui.config('bugzilla', 'bzuser')
+                userid = self.get_user_id(defaultuser)
+            except KeyError:
+                raise util.Abort(_('cannot find user id for %s or %s') %
+                                 (prefuser, defaultuser))
+        now = time.strftime('%Y-%m-%d %H:%M:%S')
+        self.run('''insert into longdescs
+                    (bug_id, who, bug_when, thetext)
+                    values (%s, %s, %s, %s)''',
+                 (bugid, userid, now, text))
+        self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
+                    values (%s, %s, %s, %s)''',
+                 (bugid, userid, now, self.longdesc_id))
+
+class bugzilla(object):
+    # supported versions of bugzilla. different versions have
+    # different schemas.
+    _versions = {
+        '2.16': bugzilla_2_16,
+        }
+
+    _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
+                       r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
+
+    _bz = None
+
+    def __init__(self, ui, repo):
+        self.ui = ui
+        self.repo = repo
+
+    def bz(self):
+        '''return object that knows how to talk to bugzilla version in
+        use.'''
+
+        if bugzilla._bz is None:
+            bzversion = self.ui.config('bugzilla', 'version')
+            try:
+                bzclass = bugzilla._versions[bzversion]
+            except KeyError:
+                raise util.Abort(_('bugzilla version %s not supported') %
+                                 bzversion)
+            bugzilla._bz = bzclass(self.ui)
+        return bugzilla._bz
+
+    def __getattr__(self, key):
+        return getattr(self.bz(), key)
+
+    _bug_re = None
+    _split_re = None
+
+    def find_bug_ids(self, node, desc):
+        '''find valid bug ids that are referred to in changeset
+        comments and that do not already have references to this
+        changeset.'''
+
+        if bugzilla._bug_re is None:
+            bugzilla._bug_re = re.compile(
+                self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
+                re.IGNORECASE)
+            bugzilla._split_re = re.compile(r'\D+')
+        start = 0
+        ids = {}
+        while True:
+            m = bugzilla._bug_re.search(desc, start)
+            if not m:
+                break
+            start = m.end()
+            for id in bugzilla._split_re.split(m.group(1)):
+                ids[int(id)] = 1
+        ids = ids.keys()
+        if ids:
+            ids = self.filter_real_bug_ids(ids)
+        if ids:
+            ids = self.filter_unknown_bug_ids(node, ids)
+        return ids
+
+    def update(self, bugid, node, changes):
+        '''update bugzilla bug with reference to changeset.'''
+
+        def webroot(root):
+            '''strip leading prefix of repo root and turn into
+            url-safe path.'''
+            count = int(self.ui.config('bugzilla', 'strip', 0))
+            root = util.pconvert(root)
+            while count > 0:
+                c = root.find('/')
+                if c == -1:
+                    break
+                root = root[c+1:]
+                count -= 1
+            return root
+
+        class stringio(object):
+            '''wrap cStringIO.'''
+            def __init__(self):
+                self.fp = cStringIO.StringIO()
+
+            def write(self, *args):
+                for a in args:
+                    self.fp.write(a)
+
+            write_header = write
+
+            def getvalue(self):
+                return self.fp.getvalue()
+
+        mapfile = self.ui.config('bugzilla', 'style')
+        tmpl = self.ui.config('bugzilla', 'template')
+        sio = stringio()
+        t = templater.changeset_templater(self.ui, self.repo, mapfile, sio)
+        if not mapfile and not tmpl:
+            tmpl = _('changeset {node|short} in repo {root} refers '
+                     'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
+        if tmpl:
+            tmpl = templater.parsestring(tmpl, quoted=False)
+            t.use_template(tmpl)
+        t.show(changenode=node, changes=changes,
+               bug=str(bugid),
+               hgweb=self.ui.config('bugzilla', 'hgweb'),
+               root=self.repo.root,
+               webroot=webroot(self.repo.root))
+        self.add_comment(bugid, sio.getvalue(), templater.email(changes[1]))
+
+def hook(ui, repo, hooktype, node=None, **kwargs):
+    '''add comment to bugzilla for each changeset that refers to a
+    bugzilla bug id. only add a comment once per bug, so same change
+    seen multiple times does not fill bug with duplicate data.'''
+    if node is None:
+        raise util.Abort(_('hook type %s does not pass a changeset id') %
+                         hooktype)
+    try:
+        bz = bugzilla(ui, repo)
+        bin_node = bin(node)
+        changes = repo.changelog.read(bin_node)
+        ids = bz.find_bug_ids(bin_node, changes[4])
+        if ids:
+            for id in ids:
+                bz.update(id, bin_node, changes)
+            bz.notify(ids)
+        return True
+    except MySQLdb.MySQLError, err:
+        raise util.Abort(_('database error: %s') % err[1])
+
--- a/hgext/mq.py	Wed May 03 22:47:08 2006 -0700
+++ b/hgext/mq.py	Wed May 03 22:47:57 2006 -0700
@@ -1233,34 +1233,28 @@
     repomap[repo] = queue(ui, repo.join(""))
 
 cmdtable = {
-    "qapplied": (applied, [], 'hg qapplied [patch]'),
+    "qapplied": (applied, [], 'hg qapplied [PATCH]'),
     "qcommit|qci":
         (commit,
-         [('A', 'addremove', None, _('run addremove during commit')),
-          ('I', 'include', [], _('include names matching the given patterns')),
-          ('X', 'exclude', [], _('exclude names matching the given patterns')),
-          ('m', 'message', '', _('use <text> as commit message')),
-          ('l', 'logfile', '', _('read the commit message from <file>')),
-          ('d', 'date', '', _('record datecode as commit date')),
-          ('u', 'user', '', _('record user as commiter'))],
-         'hg qcommit [options] [files]'),
-    "^qdiff": (diff, [], 'hg qdiff [files]'),
-    "qdelete": (delete, [], 'hg qdelete [patch]'),
+         commands.table["^commit|ci"][1],
+         'hg qcommit [OPTION]... [FILE]...'),
+    "^qdiff": (diff, [], 'hg qdiff [FILE]...'),
+    "qdelete": (delete, [], 'hg qdelete PATCH'),
     "^qimport":
         (qimport,
          [('e', 'existing', None, 'import file in patch dir'),
           ('n', 'name', '', 'patch file name'),
           ('f', 'force', None, 'overwrite existing files')],
-         'hg qimport'),
+         'hg qimport [-e] [-n NAME] [-f] FILE...'),
     "^qinit":
         (init,
          [('c', 'create-repo', None, 'create patch repository')],
-         'hg [-c] qinit'),
+         'hg qinit [-c]'),
     "qnew":
         (new,
          [('m', 'message', '', 'commit message'),
           ('f', 'force', None, 'force')],
-         'hg qnew [-m message ] patch'),
+         'hg qnew [-m TEXT] [-f] PATCH'),
     "qnext": (next, [], 'hg qnext'),
     "qprev": (prev, [], 'hg qprev'),
     "^qpop":
@@ -1268,7 +1262,7 @@
          [('a', 'all', None, 'pop all patches'),
           ('n', 'name', '', 'queue name to pop'),
           ('f', 'force', None, 'forget any local changes')],
-         'hg qpop [options] [patch/index]'),
+         'hg qpop [-a] [-n NAME] [-f] [PATCH | INDEX]'),
     "^qpush":
         (push,
          [('f', 'force', None, 'apply if the patch has rejects'),
@@ -1276,16 +1270,16 @@
           ('a', 'all', None, 'apply all patches'),
           ('m', 'merge', None, 'merge from another queue'),
           ('n', 'name', '', 'merge queue name')],
-         'hg qpush [options] [patch/index]'),
+         'hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]'),
     "^qrefresh":
         (refresh,
          [('s', 'short', None, 'short refresh')],
-         'hg qrefresh'),
+         'hg qrefresh [-s]'),
     "qrestore":
         (restore,
          [('d', 'delete', None, 'delete save entry'),
           ('u', 'update', None, 'update queue working dir')],
-         'hg qrestore rev'),
+         'hg qrestore [-d] [-u] REV'),
     "qsave":
         (save,
          [('m', 'message', '', 'commit message'),
@@ -1293,19 +1287,19 @@
           ('n', 'name', '', 'copy directory name'),
           ('e', 'empty', None, 'clear queue status file'),
           ('f', 'force', None, 'force copy')],
-         'hg qsave'),
+         'hg qsave [-m TEXT] [-c] [-n NAME] [-e] [-f]'),
     "qseries":
         (series,
          [('m', 'missing', None, 'print patches not in series')],
-         'hg qseries'),
+         'hg qseries [-m]'),
     "^strip":
         (strip,
          [('f', 'force', None, 'force multi-head removal'),
           ('b', 'backup', None, 'bundle unrelated changesets'),
           ('n', 'nobackup', None, 'no backups')],
-         'hg strip rev'),
+         'hg strip [-f] [-b] [-n] REV'),
     "qtop": (top, [], 'hg qtop'),
-    "qunapplied": (unapplied, [], 'hg qunapplied [patch]'),
+    "qunapplied": (unapplied, [], 'hg qunapplied [PATCH]'),
     "qversion": (version, [], 'hg qversion')
 }
 
--- a/mercurial/commands.py	Wed May 03 22:47:08 2006 -0700
+++ b/mercurial/commands.py	Wed May 03 22:47:57 2006 -0700
@@ -398,194 +398,6 @@
         user = revcache[rev] = ui.shortuser(name)
     return user
 
-class changeset_templater(object):
-    '''use templater module to format changeset information.'''
-
-    def __init__(self, ui, repo, mapfile):
-        self.t = templater.templater(mapfile, templater.common_filters,
-                                     cache={'parent': '{rev}:{node|short} ',
-                                            'manifest': '{rev}:{node|short}'})
-        self.ui = ui
-        self.repo = repo
-
-    def use_template(self, t):
-        '''set template string to use'''
-        self.t.cache['changeset'] = t
-
-    def write(self, thing, header=False):
-        '''write expanded template.
-        uses in-order recursive traverse of iterators.'''
-        for t in thing:
-            if hasattr(t, '__iter__'):
-                self.write(t, header=header)
-            elif header:
-                self.ui.write_header(t)
-            else:
-                self.ui.write(t)
-
-    def write_header(self, thing):
-        self.write(thing, header=True)
-
-    def show(self, rev=0, changenode=None, brinfo=None):
-        '''show a single changeset or file revision'''
-        log = self.repo.changelog
-        if changenode is None:
-            changenode = log.node(rev)
-        elif not rev:
-            rev = log.rev(changenode)
-
-        changes = log.read(changenode)
-
-        def showlist(name, values, plural=None, **args):
-            '''expand set of values.
-            name is name of key in template map.
-            values is list of strings or dicts.
-            plural is plural of name, if not simply name + 's'.
-
-            expansion works like this, given name 'foo'.
-
-            if values is empty, expand 'no_foos'.
-
-            if 'foo' not in template map, return values as a string,
-            joined by space.
-
-            expand 'start_foos'.
-
-            for each value, expand 'foo'. if 'last_foo' in template
-            map, expand it instead of 'foo' for last key.
-
-            expand 'end_foos'.
-            '''
-            if plural: names = plural
-            else: names = name + 's'
-            if not values:
-                noname = 'no_' + names
-                if noname in self.t:
-                    yield self.t(noname, **args)
-                return
-            if name not in self.t:
-                if isinstance(values[0], str):
-                    yield ' '.join(values)
-                else:
-                    for v in values:
-                        yield dict(v, **args)
-                return
-            startname = 'start_' + names
-            if startname in self.t:
-                yield self.t(startname, **args)
-            vargs = args.copy()
-            def one(v, tag=name):
-                try:
-                    vargs.update(v)
-                except (AttributeError, ValueError):
-                    try:
-                        for a, b in v:
-                            vargs[a] = b
-                    except ValueError:
-                        vargs[name] = v
-                return self.t(tag, **vargs)
-            lastname = 'last_' + name
-            if lastname in self.t:
-                last = values.pop()
-            else:
-                last = None
-            for v in values:
-                yield one(v)
-            if last is not None:
-                yield one(last, tag=lastname)
-            endname = 'end_' + names
-            if endname in self.t:
-                yield self.t(endname, **args)
-
-        if brinfo:
-            def showbranches(**args):
-                if changenode in brinfo:
-                    for x in showlist('branch', brinfo[changenode],
-                                      plural='branches', **args):
-                        yield x
-        else:
-            showbranches = ''
-
-        if self.ui.debugflag:
-            def showmanifest(**args):
-                args = args.copy()
-                args.update(dict(rev=self.repo.manifest.rev(changes[0]),
-                                 node=hex(changes[0])))
-                yield self.t('manifest', **args)
-        else:
-            showmanifest = ''
-
-        def showparents(**args):
-            parents = [[('rev', log.rev(p)), ('node', hex(p))]
-                       for p in log.parents(changenode)
-                       if self.ui.debugflag or p != nullid]
-            if (not self.ui.debugflag and len(parents) == 1 and
-                parents[0][0][1] == rev - 1):
-                return
-            for x in showlist('parent', parents, **args):
-                yield x
-
-        def showtags(**args):
-            for x in showlist('tag', self.repo.nodetags(changenode), **args):
-                yield x
-
-        if self.ui.debugflag:
-            files = self.repo.changes(log.parents(changenode)[0], changenode)
-            def showfiles(**args):
-                for x in showlist('file', files[0], **args): yield x
-            def showadds(**args):
-                for x in showlist('file_add', files[1], **args): yield x
-            def showdels(**args):
-                for x in showlist('file_del', files[2], **args): yield x
-        else:
-            def showfiles(**args):
-                for x in showlist('file', changes[3], **args): yield x
-            showadds = ''
-            showdels = ''
-
-        props = {
-            'author': changes[1],
-            'branches': showbranches,
-            'date': changes[2],
-            'desc': changes[4],
-            'file_adds': showadds,
-            'file_dels': showdels,
-            'files': showfiles,
-            'manifest': showmanifest,
-            'node': hex(changenode),
-            'parents': showparents,
-            'rev': rev,
-            'tags': showtags,
-            }
-
-        try:
-            if self.ui.debugflag and 'header_debug' in self.t:
-                key = 'header_debug'
-            elif self.ui.quiet and 'header_quiet' in self.t:
-                key = 'header_quiet'
-            elif self.ui.verbose and 'header_verbose' in self.t:
-                key = 'header_verbose'
-            elif 'header' in self.t:
-                key = 'header'
-            else:
-                key = ''
-            if key:
-                self.write_header(self.t(key, **props))
-            if self.ui.debugflag and 'changeset_debug' in self.t:
-                key = 'changeset_debug'
-            elif self.ui.quiet and 'changeset_quiet' in self.t:
-                key = 'changeset_quiet'
-            elif self.ui.verbose and 'changeset_verbose' in self.t:
-                key = 'changeset_verbose'
-            else:
-                key = 'changeset'
-            self.write(self.t(key, **props))
-        except KeyError, inst:
-            raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
-                                                           inst.args[0]))
-        except SyntaxError, inst:
-            raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
-
 class changeset_printer(object):
     '''show changeset information when templating not requested.'''
 
@@ -672,7 +484,7 @@
                 if not mapname: mapname = templater.templatepath(mapfile)
                 if mapname: mapfile = mapname
         try:
-            t = changeset_templater(ui, repo, mapfile)
+            t = templater.changeset_templater(ui, repo, mapfile)
         except SyntaxError, inst:
             raise util.Abort(inst.args[0])
         if tmpl: t.use_template(tmpl)
@@ -809,13 +621,19 @@
     repo.add(names)
 
 def addremove(ui, repo, *pats, **opts):
-    """add all new files, delete all missing files
-
+    """add all new files, delete all missing files (DEPRECATED)
+
+    (DEPRECATED)
     Add all new files and remove all missing files from the repository.
 
     New files are ignored if they match any of the patterns in .hgignore. As
     with add, these changes take effect at the next commit.
+
+    This command is now deprecated and will be removed in a future
+    release. Please use add and remove --after instead.
     """
+    ui.warn(_('(the addremove command is deprecated; use add and remove '
+              '--after instead)\n'))
     return addremove_lock(ui, repo, pats, opts)
 
 def addremove_lock(ui, repo, pats, opts, wlock=None):
@@ -1153,7 +971,7 @@
                              (logfile, inst.strerror))
 
     if opts['addremove']:
-        addremove(ui, repo, *pats, **opts)
+        addremove_lock(ui, repo, pats, opts)
     fns, match, anypats = matchpats(repo, pats, opts)
     if pats:
         modified, added, removed, deleted, unknown = (
@@ -1894,7 +1712,7 @@
         files = util.patch(strip, pf, ui)
 
         if len(files) > 0:
-            addremove(ui, repo, *files)
+            addremove_lock(ui, repo, files, {})
         repo.commit(files, message, user)
 
 def incoming(ui, repo, source="default", **opts):
@@ -2347,7 +2165,7 @@
         return repo.verify()
     return 1
 
-def remove(ui, repo, pat, *pats, **opts):
+def remove(ui, repo, *pats, **opts):
     """remove the specified files on the next commit
 
     Schedule the indicated files for removal from the repository.
@@ -2355,29 +2173,36 @@
     This command schedules the files to be removed at the next commit.
     This only removes files from the current branch, not from the
     entire project history.  If the files still exist in the working
-    directory, they will be deleted from it.
+    directory, they will be deleted from it.  If invoked with --after,
+    files that have been manually deleted are marked as removed.
     """
     names = []
+    if not opts['after'] and not pats:
+        raise util.Abort(_('no files specified'))
     def okaytoremove(abs, rel, exact):
         modified, added, removed, deleted, unknown = repo.changes(files=[abs])
         reason = None
-        if modified and not opts['force']:
+        if not deleted and opts['after']:
+            reason = _('is still present')
+        elif modified and not opts['force']:
             reason = _('is modified')
         elif added:
             reason = _('has been marked for add')
         elif unknown:
             reason = _('is not managed')
+        elif removed:
+            return False
         if reason:
             if exact:
                 ui.warn(_('not removing %s: file %s\n') % (rel, reason))
         else:
             return True
-    for src, abs, rel, exact in walk(repo, (pat,) + pats, opts):
+    for src, abs, rel, exact in walk(repo, pats, opts):
         if okaytoremove(abs, rel, exact):
             if ui.verbose or not exact:
                 ui.status(_('removing %s\n') % rel)
             names.append(abs)
-    repo.remove(names, unlink=True)
+    repo.remove(names, unlink=not opts['after'])
 
 def rename(ui, repo, *pats, **opts):
     """rename files; equivalent of copy + remove
@@ -2916,7 +2741,7 @@
          [('I', 'include', [], _('include names matching the given patterns')),
           ('X', 'exclude', [], _('exclude names matching the given patterns'))],
          _('hg add [OPTION]... [FILE]...')),
-    "addremove":
+    "debugaddremove|addremove":
         (addremove,
          [('I', 'include', [], _('include names matching the given patterns')),
           ('X', 'exclude', [], _('exclude names matching the given patterns'))],
@@ -2976,7 +2801,8 @@
          _('hg clone [OPTION]... SOURCE [DEST]')),
     "^commit|ci":
         (commit,
-         [('A', 'addremove', None, _('run addremove during commit')),
+         [('A', 'addremove', None,
+           _('mark new/missing files as added/removed before committing')),
           ('m', 'message', '', _('use <text> as commit message')),
           ('l', 'logfile', '', _('read the commit message from <file>')),
           ('d', 'date', '', _('record datecode as commit date')),
@@ -3161,7 +2987,8 @@
     "recover": (recover, [], _('hg recover')),
     "^remove|rm":
         (remove,
-         [('f', 'force', None, _('remove file even if modified')),
+         [('', 'after', None, _('record remove that has already occurred')),
+          ('f', 'force', None, _('remove file even if modified')),
           ('I', 'include', [], _('include names matching the given patterns')),
           ('X', 'exclude', [], _('exclude names matching the given patterns'))],
          _('hg remove [OPTION]... FILE...')),
--- a/mercurial/localrepo.py	Wed May 03 22:47:08 2006 -0700
+++ b/mercurial/localrepo.py	Wed May 03 22:47:57 2006 -0700
@@ -105,7 +105,7 @@
                                    '("%s" is not callable)') %
                                  (hname, funcname))
             try:
-                r = obj(ui=ui, repo=repo, hooktype=name, **args)
+                r = obj(ui=self.ui, repo=self, hooktype=name, **args)
             except (KeyboardInterrupt, util.SignalInterrupt):
                 raise
             except Exception, exc:
--- a/mercurial/templater.py	Wed May 03 22:47:08 2006 -0700
+++ b/mercurial/templater.py	Wed May 03 22:47:57 2006 -0700
@@ -8,6 +8,7 @@
 import re
 from demandload import demandload
 from i18n import gettext as _
+from node import *
 demandload(globals(), "cStringIO cgi re sys os time urllib util textwrap")
 
 esctable = {
@@ -209,7 +210,7 @@
                 break
             yield text[start:m.start(0)], m.group(1)
             start = m.end(1)
-            
+
     fp = cStringIO.StringIO()
     for para, rest in findparas():
         fp.write(space_re.sub(' ', textwrap.fill(para, width)))
@@ -241,7 +242,7 @@
     r = author.find('>')
     if r == -1: r = None
     return author[author.find('<')+1:r]
-    
+
 def person(author):
     '''get name of author, or else username.'''
     f = author.find('<')
@@ -267,6 +268,7 @@
 
 common_filters = {
     "addbreaks": nl2br,
+    "basename": os.path.basename,
     "age": age,
     "date": lambda x: util.datestr(x),
     "domain": domain,
@@ -292,6 +294,7 @@
 def templatepath(name=None):
     '''return location of template file or directory (if no name).
     returns None if not found.'''
+
     # executable version (py2exe) doesn't support __file__
     if hasattr(sys, 'frozen'):
         module = sys.executable
@@ -303,3 +306,196 @@
         p = os.path.join(os.path.dirname(module), *fl)
         if (name and os.path.exists(p)) or os.path.isdir(p):
             return os.path.normpath(p)
+
+class changeset_templater(object):
+    '''format changeset information.'''
+
+    def __init__(self, ui, repo, mapfile, dest=None):
+        self.t = templater(mapfile, common_filters,
+                           cache={'parent': '{rev}:{node|short} ',
+                                  'manifest': '{rev}:{node|short}'})
+        self.ui = ui
+        self.dest = dest
+        self.repo = repo
+
+    def use_template(self, t):
+        '''set template string to use'''
+        self.t.cache['changeset'] = t
+
+    def write(self, thing, header=False):
+        '''write expanded template.
+        uses in-order recursive traverse of iterators.'''
+        dest = self.dest or self.ui
+        for t in thing:
+            if hasattr(t, '__iter__'):
+                self.write(t, header=header)
+            elif header:
+                dest.write_header(t)
+            else:
+                dest.write(t)
+
+    def write_header(self, thing):
+        self.write(thing, header=True)
+
+    def show(self, rev=0, changenode=None, brinfo=None, changes=None,
+             **props):
+        '''show a single changeset or file revision'''
+        log = self.repo.changelog
+        if changenode is None:
+            changenode = log.node(rev)
+        elif not rev:
+            rev = log.rev(changenode)
+        if changes is None:
+            changes = log.read(changenode)
+
+        def showlist(name, values, plural=None, **args):
+            '''expand set of values.
+            name is name of key in template map.
+            values is list of strings or dicts.
+            plural is plural of name, if not simply name + 's'.
+
+            expansion works like this, given name 'foo'.
+
+            if values is empty, expand 'no_foos'.
+
+            if 'foo' not in template map, return values as a string,
+            joined by space.
+
+            expand 'start_foos'.
+
+            for each value, expand 'foo'. if 'last_foo' in template
+            map, expand it instead of 'foo' for last key.
+
+            expand 'end_foos'.
+            '''
+            if plural: names = plural
+            else: names = name + 's'
+            if not values:
+                noname = 'no_' + names
+                if noname in self.t:
+                    yield self.t(noname, **args)
+                return
+            if name not in self.t:
+                if isinstance(values[0], str):
+                    yield ' '.join(values)
+                else:
+                    for v in values:
+                        yield dict(v, **args)
+                return
+            startname = 'start_' + names
+            if startname in self.t:
+                yield self.t(startname, **args)
+            vargs = args.copy()
+            def one(v, tag=name):
+                try:
+                    vargs.update(v)
+                except (AttributeError, ValueError):
+                    try:
+                        for a, b in v:
+                            vargs[a] = b
+                    except ValueError:
+                        vargs[name] = v
+                return self.t(tag, **vargs)
+            lastname = 'last_' + name
+            if lastname in self.t:
+                last = values.pop()
+            else:
+                last = None
+            for v in values:
+                yield one(v)
+            if last is not None:
+                yield one(last, tag=lastname)
+            endname = 'end_' + names
+            if endname in self.t:
+                yield self.t(endname, **args)
+
+        if brinfo:
+            def showbranches(**args):
+                if changenode in brinfo:
+                    for x in showlist('branch', brinfo[changenode],
+                                      plural='branches', **args):
+                        yield x
+        else:
+            showbranches = ''
+
+        if self.ui.debugflag:
+            def showmanifest(**args):
+                args = args.copy()
+                args.update(dict(rev=self.repo.manifest.rev(changes[0]),
+                                 node=hex(changes[0])))
+                yield self.t('manifest', **args)
+        else:
+            showmanifest = ''
+
+        def showparents(**args):
+            parents = [[('rev', log.rev(p)), ('node', hex(p))]
+                       for p in log.parents(changenode)
+                       if self.ui.debugflag or p != nullid]
+            if (not self.ui.debugflag and len(parents) == 1 and
+                parents[0][0][1] == rev - 1):
+                return
+            for x in showlist('parent', parents, **args):
+                yield x
+
+        def showtags(**args):
+            for x in showlist('tag', self.repo.nodetags(changenode), **args):
+                yield x
+
+        if self.ui.debugflag:
+            files = self.repo.changes(log.parents(changenode)[0], changenode)
+            def showfiles(**args):
+                for x in showlist('file', files[0], **args): yield x
+            def showadds(**args):
+                for x in showlist('file_add', files[1], **args): yield x
+            def showdels(**args):
+                for x in showlist('file_del', files[2], **args): yield x
+        else:
+            def showfiles(**args):
+                for x in showlist('file', changes[3], **args): yield x
+            showadds = ''
+            showdels = ''
+
+        defprops = {
+            'author': changes[1],
+            'branches': showbranches,
+            'date': changes[2],
+            'desc': changes[4],
+            'file_adds': showadds,
+            'file_dels': showdels,
+            'files': showfiles,
+            'manifest': showmanifest,
+            'node': hex(changenode),
+            'parents': showparents,
+            'rev': rev,
+            'tags': showtags,
+            }
+        props = props.copy()
+        props.update(defprops)
+
+        try:
+            if self.ui.debugflag and 'header_debug' in self.t:
+                key = 'header_debug'
+            elif self.ui.quiet and 'header_quiet' in self.t:
+                key = 'header_quiet'
+            elif self.ui.verbose and 'header_verbose' in self.t:
+                key = 'header_verbose'
+            elif 'header' in self.t:
+                key = 'header'
+            else:
+                key = ''
+            if key:
+                self.write_header(self.t(key, **props))
+            if self.ui.debugflag and 'changeset_debug' in self.t:
+                key = 'changeset_debug'
+            elif self.ui.quiet and 'changeset_quiet' in self.t:
+                key = 'changeset_quiet'
+            elif self.ui.verbose and 'changeset_verbose' in self.t:
+                key = 'changeset_verbose'
+            else:
+                key = 'changeset'
+            self.write(self.t(key, **props))
+        except KeyError, inst:
+            raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
+                                                           inst.args[0]))
+        except SyntaxError, inst:
+            raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
--- a/tests/run-tests	Wed May 03 22:47:08 2006 -0700
+++ b/tests/run-tests	Wed May 03 22:47:57 2006 -0700
@@ -69,7 +69,7 @@
 INST="$HGTMP/install"
 PYTHONDIR="$INST/lib/python"
 cd ..
-if ${PYTHON-python} setup.py install --home="$INST" \
+if ${PYTHON-python} setup.py clean --all install --force --home="$INST" \
   --install-lib="$PYTHONDIR" > tests/install.err 2>&1
 then
     rm tests/install.err
--- a/tests/run-tests.py	Wed May 03 22:47:08 2006 -0700
+++ b/tests/run-tests.py	Wed May 03 22:47:57 2006 -0700
@@ -69,8 +69,9 @@
     installerrs = os.path.join("tests", "install.err")
 
     os.chdir("..") # Get back to hg root
-    cmd = '%s setup.py install --home="%s" --install-lib="%s" >%s 2>&1' % \
-        (sys.executable, INST, PYTHONDIR, installerrs)
+    cmd = ('%s setup.py clean --all'
+           ' install --force --home="%s" --install-lib="%s" >%s 2>&1'
+           % (sys.executable, INST, PYTHONDIR, installerrs))
     vlog("# Running", cmd)
     if os.system(cmd) == 0:
         if not verbose:
--- a/tests/test-addremove.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-addremove.out	Wed May 03 22:47:57 2006 -0700
@@ -1,7 +1,9 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding dir/bar
 adding foo
 dir/bar
 foo
+(the addremove command is deprecated; use add and remove --after instead)
 adding dir/bar_2
 adding foo_2
 dir/bar_2
--- a/tests/test-archive	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-archive	Wed May 03 22:47:57 2006 -0700
@@ -29,8 +29,8 @@
                     % (node, archive))
 sys.stdout.write(f.read())
 EOF
-http_proxy= python getarchive.py "$TIP" gz | gunzip -dc - | tar tf - | sed "s/$QTIP/TIP/"
-http_proxy= python getarchive.py "$TIP" bz2 | bunzip2 -dc - | tar tf - | sed "s/$QTIP/TIP/"
+http_proxy= python getarchive.py "$TIP" gz | gunzip | tar tf - | sed "s/$QTIP/TIP/"
+http_proxy= python getarchive.py "$TIP" bz2 | bunzip2 | tar tf - | sed "s/$QTIP/TIP/"
 http_proxy= python getarchive.py "$TIP" zip > archive.zip
 unzip -t archive.zip | sed "s/$QTIP/TIP/"
 
--- a/tests/test-archive.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-archive.out	Wed May 03 22:47:57 2006 -0700
@@ -1,5 +1,8 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding foo
+(the addremove command is deprecated; use add and remove --after instead)
 adding bar
+(the addremove command is deprecated; use add and remove --after instead)
 adding baz/bletch
 test-archive-TIP/.hg_archival.txt
 test-archive-TIP/bar
--- a/tests/test-backout	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-backout	Wed May 03 22:47:57 2006 -0700
@@ -28,7 +28,7 @@
 echo '# backout of backout is as if nothing happened'
 
 hg backout -d '3 0' --merge tip
-cat a
+cat a 2>/dev/null || echo cat: a: No such file or directory
 
 echo '# backout with merge'
 cd ..
--- a/tests/test-help.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-help.out	Wed May 03 22:47:57 2006 -0700
@@ -38,92 +38,90 @@
 
 list of commands (use "hg help -v" to show aliases and global options):
 
- add         add the specified files on the next commit
- addremove   add all new files, delete all missing files
- annotate    show changeset information per file line
- archive     create unversioned archive of a repository revision
- backout     reverse effect of earlier changeset
- bundle      create a changegroup file
- cat         output the latest or given revisions of files
- clone       make a copy of an existing repository
- commit      commit the specified files or all outstanding changes
- copy        mark files as copied for the next commit
- diff        diff repository (or selected files)
- export      dump the header and diffs for one or more changesets
- grep        search for a pattern in specified files and revisions
- heads       show current repository heads
- help        show help for a given command or all commands
- identify    print information about the working copy
- import      import an ordered set of patches
- incoming    show new changesets found in source
- init        create a new repository in the given directory
- locate      locate files matching specific patterns
- log         show revision history of entire repository or files
- manifest    output the latest or given revision of the project manifest
- merge       Merge working directory with another revision
- outgoing    show changesets not found in destination
- parents     show the parents of the working dir or revision
- paths       show definition of symbolic path names
- pull        pull changes from the specified source
- push        push changes to the specified destination
- recover     roll back an interrupted transaction
- remove      remove the specified files on the next commit
- rename      rename files; equivalent of copy + remove
- revert      revert modified files or dirs back to their unmodified states
- root        print the root (top) of the current working dir
- serve       export the repository via HTTP
- status      show changed files in the working directory
- tag         add a tag for the current tip or a given revision
- tags        list repository tags
- tip         show the tip revision
- unbundle    apply a changegroup file
- undo        undo the last commit or pull
- update      update or merge working directory
- verify      verify the integrity of the repository
- version     output version and copyright information
- add         add the specified files on the next commit
- addremove   add all new files, delete all missing files
- annotate    show changeset information per file line
- archive     create unversioned archive of a repository revision
- backout     reverse effect of earlier changeset
- bundle      create a changegroup file
- cat         output the latest or given revisions of files
- clone       make a copy of an existing repository
- commit      commit the specified files or all outstanding changes
- copy        mark files as copied for the next commit
- diff        diff repository (or selected files)
- export      dump the header and diffs for one or more changesets
- grep        search for a pattern in specified files and revisions
- heads       show current repository heads
- help        show help for a given command or all commands
- identify    print information about the working copy
- import      import an ordered set of patches
- incoming    show new changesets found in source
- init        create a new repository in the given directory
- locate      locate files matching specific patterns
- log         show revision history of entire repository or files
- manifest    output the latest or given revision of the project manifest
- merge       Merge working directory with another revision
- outgoing    show changesets not found in destination
- parents     show the parents of the working dir or revision
- paths       show definition of symbolic path names
- pull        pull changes from the specified source
- push        push changes to the specified destination
- recover     roll back an interrupted transaction
- remove      remove the specified files on the next commit
- rename      rename files; equivalent of copy + remove
- revert      revert modified files or dirs back to their unmodified states
- root        print the root (top) of the current working dir
- serve       export the repository via HTTP
- status      show changed files in the working directory
- tag         add a tag for the current tip or a given revision
- tags        list repository tags
- tip         show the tip revision
- unbundle    apply a changegroup file
- undo        undo the last commit or pull
- update      update or merge working directory
- verify      verify the integrity of the repository
- version     output version and copyright information
+ add        add the specified files on the next commit
+ annotate   show changeset information per file line
+ archive    create unversioned archive of a repository revision
+ backout    reverse effect of earlier changeset
+ bundle     create a changegroup file
+ cat        output the latest or given revisions of files
+ clone      make a copy of an existing repository
+ commit     commit the specified files or all outstanding changes
+ copy       mark files as copied for the next commit
+ diff       diff repository (or selected files)
+ export     dump the header and diffs for one or more changesets
+ grep       search for a pattern in specified files and revisions
+ heads      show current repository heads
+ help       show help for a given command or all commands
+ identify   print information about the working copy
+ import     import an ordered set of patches
+ incoming   show new changesets found in source
+ init       create a new repository in the given directory
+ locate     locate files matching specific patterns
+ log        show revision history of entire repository or files
+ manifest   output the latest or given revision of the project manifest
+ merge      Merge working directory with another revision
+ outgoing   show changesets not found in destination
+ parents    show the parents of the working dir or revision
+ paths      show definition of symbolic path names
+ pull       pull changes from the specified source
+ push       push changes to the specified destination
+ recover    roll back an interrupted transaction
+ remove     remove the specified files on the next commit
+ rename     rename files; equivalent of copy + remove
+ revert     revert modified files or dirs back to their unmodified states
+ root       print the root (top) of the current working dir
+ serve      export the repository via HTTP
+ status     show changed files in the working directory
+ tag        add a tag for the current tip or a given revision
+ tags       list repository tags
+ tip        show the tip revision
+ unbundle   apply a changegroup file
+ undo       undo the last commit or pull
+ update     update or merge working directory
+ verify     verify the integrity of the repository
+ version    output version and copyright information
+ add        add the specified files on the next commit
+ annotate   show changeset information per file line
+ archive    create unversioned archive of a repository revision
+ backout    reverse effect of earlier changeset
+ bundle     create a changegroup file
+ cat        output the latest or given revisions of files
+ clone      make a copy of an existing repository
+ commit     commit the specified files or all outstanding changes
+ copy       mark files as copied for the next commit
+ diff       diff repository (or selected files)
+ export     dump the header and diffs for one or more changesets
+ grep       search for a pattern in specified files and revisions
+ heads      show current repository heads
+ help       show help for a given command or all commands
+ identify   print information about the working copy
+ import     import an ordered set of patches
+ incoming   show new changesets found in source
+ init       create a new repository in the given directory
+ locate     locate files matching specific patterns
+ log        show revision history of entire repository or files
+ manifest   output the latest or given revision of the project manifest
+ merge      Merge working directory with another revision
+ outgoing   show changesets not found in destination
+ parents    show the parents of the working dir or revision
+ paths      show definition of symbolic path names
+ pull       pull changes from the specified source
+ push       push changes to the specified destination
+ recover    roll back an interrupted transaction
+ remove     remove the specified files on the next commit
+ rename     rename files; equivalent of copy + remove
+ revert     revert modified files or dirs back to their unmodified states
+ root       print the root (top) of the current working dir
+ serve      export the repository via HTTP
+ status     show changed files in the working directory
+ tag        add a tag for the current tip or a given revision
+ tags       list repository tags
+ tip        show the tip revision
+ unbundle   apply a changegroup file
+ undo       undo the last commit or pull
+ update     update or merge working directory
+ verify     verify the integrity of the repository
+ version    output version and copyright information
 hg add [OPTION]... [FILE]...
 
 add the specified files on the next commit
--- a/tests/test-pull.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-pull.out	Wed May 03 22:47:57 2006 -0700
@@ -1,3 +1,4 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding foo
 checking changesets
 checking manifests
--- a/tests/test-remove	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-remove	Wed May 03 22:47:57 2006 -0700
@@ -5,8 +5,12 @@
 echo a > foo
 hg add foo
 hg commit -m 1 -d "1000000 0"
+hg remove
 rm foo
 hg remove foo
+hg revert
+rm foo
+hg remove --after
 hg commit -m 2 -d "1000000 0"
 hg export 0
 hg export 1
--- a/tests/test-remove.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-remove.out	Wed May 03 22:47:57 2006 -0700
@@ -1,3 +1,6 @@
+abort: no files specified
+undeleting foo
+removing foo
 # HG changeset patch
 # User test
 # Node ID 8ba83d44753d6259db5ce6524974dd1174e90f47
--- a/tests/test-simple-update.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-simple-update.out	Wed May 03 22:47:57 2006 -0700
@@ -1,3 +1,4 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding foo
 checking changesets
 checking manifests
--- a/tests/test-symlinks.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-symlinks.out	Wed May 03 22:47:57 2006 -0700
@@ -1,4 +1,6 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding foo
+(the addremove command is deprecated; use add and remove --after instead)
 adding bomb
 adding a.c
 adding dir/a.o
--- a/tests/test-up-local-change.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-up-local-change.out	Wed May 03 22:47:57 2006 -0700
@@ -1,3 +1,4 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding a
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
@@ -7,6 +8,7 @@
 @@ -1,1 +1,1 @@ a
 -a
 +abc
+(the addremove command is deprecated; use add and remove --after instead)
 adding b
 M a
 changeset:   0:33aaa84a386b
@@ -88,6 +90,7 @@
 -a2
 +abc
 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+(the addremove command is deprecated; use add and remove --after instead)
 adding b
 M a
 changeset:   1:802f095af299
--- a/tests/test-walk.out	Wed May 03 22:47:08 2006 -0700
+++ b/tests/test-walk.out	Wed May 03 22:47:57 2006 -0700
@@ -1,3 +1,4 @@
+(the addremove command is deprecated; use add and remove --after instead)
 adding beans/black
 adding beans/borlotti
 adding beans/kidney