changeset 9167:b67adc2daa15

Merge with hg
author Brendan Cully <brendan@kublai.com>
date Thu, 16 Jul 2009 21:05:24 -0700
parents 2399362b3bb0 (current diff) 47ce7a3a1fb0 (diff)
children 7a276f72a08a
files hgext/mq.py
diffstat 14 files changed, 719 insertions(+), 213 deletions(-) [+]
line wrap: on
line diff
--- a/contrib/perf.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/contrib/perf.py	Thu Jul 16 21:05:24 2009 -0700
@@ -51,7 +51,7 @@
     def t():
         repo.changelog = mercurial.changelog.changelog(repo.sopener)
         repo.manifest = mercurial.manifest.manifest(repo.sopener)
-        repo.tagscache = None
+        repo._tags = None
         return len(repo.tags())
     timer(t)
 
--- a/hgext/bookmarks.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/hgext/bookmarks.py	Thu Jul 16 21:05:24 2009 -0700
@@ -293,14 +293,11 @@
                 write(self, marks)
             return result
 
-        def tags(self):
+        def _findtags(self):
             """Merge bookmarks with normal tags"""
-            if self.tagscache:
-                return self.tagscache
-
-            tagscache = super(bookmark_repo, self).tags()
-            tagscache.update(parse(self))
-            return tagscache
+            (tags, tagtypes) = super(bookmark_repo, self)._findtags()
+            tags.update(parse(self))
+            return (tags, tagtypes)
 
     repo.__class__ = bookmark_repo
 
--- a/hgext/mq.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/hgext/mq.py	Thu Jul 16 21:05:24 2009 -0700
@@ -2415,34 +2415,33 @@
                 raise util.Abort(_('source has mq patches applied'))
             return super(mqrepo, self).push(remote, force, revs)
 
-        def tags(self):
-            if self.tagscache:
-                return self.tagscache
-
-            tagscache = super(mqrepo, self).tags()
+        def _findtags(self):
+            '''augment tags from base class with patch tags'''
+            result = super(mqrepo, self)._findtags()
 
             q = self.mq
             if not q.applied:
-                return tagscache
+                return result
 
             mqtags = [(bin(patch.rev), patch.name) for patch in q.applied]
 
             if mqtags[-1][0] not in self.changelog.nodemap:
                 self.ui.warn(_('mq status file refers to unknown node %s\n')
                              % short(mqtags[-1][0]))
-                return tagscache
+                return result
 
             mqtags.append((mqtags[-1][0], 'qtip'))
             mqtags.append((mqtags[0][0], 'qbase'))
             mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
+            tags = result[0]
             for patch in mqtags:
-                if patch[1] in tagscache:
+                if patch[1] in tags:
                     self.ui.warn(_('Tag %s overrides mq patch of the same name\n')
                                  % patch[1])
                 else:
-                    tagscache[patch[1]] = patch[0]
+                    tags[patch[1]] = patch[0]
 
-            return tagscache
+            return result
 
         def _branchtags(self, partial, lrev):
             q = self.mq
--- a/hgext/win32mbcs.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/hgext/win32mbcs.py	Thu Jul 16 21:05:24 2009 -0700
@@ -49,6 +49,9 @@
         return tuple(map(decode, arg))
     elif isinstance(arg, list):
         return map(decode, arg)
+    elif isinstance(arg, dict):
+        for k, v in arg.items():
+            arg[k] = decode(v)
     return arg
 
 def encode(arg):
@@ -58,29 +61,50 @@
         return tuple(map(encode, arg))
     elif isinstance(arg, list):
         return map(encode, arg)
+    elif isinstance(arg, dict):
+        for k, v in arg.items():
+            arg[k] = encode(v)
     return arg
 
-def wrapper(func, args):
+def appendsep(s):
+    # ensure the path ends with os.sep, appending it if necessary.
+    try:
+        us = decode(s)
+    except UnicodeError:
+        us = s
+    if us and us[-1] not in ':/\\':
+        s += os.sep
+    return s
+
+def wrapper(func, args, kwds):
     # check argument is unicode, then call original
     for arg in args:
         if isinstance(arg, unicode):
-            return func(*args)
+            return func(*args, **kwds)
 
     try:
         # convert arguments to unicode, call func, then convert back
-        return encode(func(*decode(args)))
+        return encode(func(*decode(args), **decode(kwds)))
     except UnicodeError:
-        # If not encoded with encoding.encoding, report it then
-        # continue with calling original function.
-        raise util.Abort(_("[win32mbcs] filename conversion fail with"
+        raise util.Abort(_("[win32mbcs] filename conversion failed with"
                          " %s encoding\n") % (encoding.encoding))
 
-def wrapname(name):
+def wrapperforlistdir(func, args, kwds):
+    # Ensure 'path' argument ends with os.sep to avoids
+    # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
+    if args:
+        args = list(args)
+        args[0] = appendsep(args[0])
+    if kwds.has_key('path'):
+        kwds['path'] = appendsep(kwds['path'])
+    return func(*args, **kwds)
+
+def wrapname(name, wrapper):
     module, name = name.rsplit('.', 1)
     module = sys.modules[module]
     func = getattr(module, name)
-    def f(*args):
-        return wrapper(func, args)
+    def f(*args, **kwds):
+        return wrapper(func, args, kwds)
     try:
         f.__name__ = func.__name__                # fail with python23
     except Exception:
@@ -110,7 +134,8 @@
     # fake is only for relevant environment.
     if encoding.encoding.lower() in problematic_encodings.split():
         for f in funcs.split():
-            wrapname(f)
+            wrapname(f, wrapper)
+        wrapname("mercurial.osutil.listdir", wrapperforlistdir)
         ui.debug(_("[win32mbcs] activated with encoding: %s\n")
                  % encoding.encoding)
 
--- a/mercurial/localrepo.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/mercurial/localrepo.py	Thu Jul 16 21:05:24 2009 -0700
@@ -13,6 +13,7 @@
 import util, extensions, hook, error
 import match as match_
 import merge as merge_
+import tags as tags_
 from lock import release
 import weakref, stat, errno, os, time, inspect
 propertycache = util.propertycache
@@ -89,8 +90,14 @@
         self.sjoin = self.store.join
         self.opener.createmode = self.store.createmode
 
-        self.tagscache = None
-        self._tagstypecache = None
+        # These two define the set of tags for this repository.  _tags
+        # maps tag name to node; _tagtypes maps tag name to 'global' or
+        # 'local'.  (Global tags are defined by .hgtags across all
+        # heads, and local tags are defined in .hg/localtags.)  They
+        # constitute the in-memory cache of tags.
+        self._tags = None
+        self._tagtypes = None
+
         self.branchcache = None
         self._ubranchcache = None  # UTF-8 version of branchcache
         self._branchcachetip = None
@@ -160,8 +167,8 @@
                 fp.write('\n')
             for name in names:
                 m = munge and munge(name) or name
-                if self._tagstypecache and name in self._tagstypecache:
-                    old = self.tagscache.get(name, nullid)
+                if self._tagtypes and name in self._tagtypes:
+                    old = self._tags.get(name, nullid)
                     fp.write('%s %s\n' % (hex(old), m))
                 fp.write('%s %s\n' % (hex(node), m))
             fp.close()
@@ -233,100 +240,43 @@
 
     def tags(self):
         '''return a mapping of tag to node'''
-        if self.tagscache:
-            return self.tagscache
+        if self._tags is None:
+            (self._tags, self._tagtypes) = self._findtags()
+
+        return self._tags
 
-        globaltags = {}
+    def _findtags(self):
+        '''Do the hard work of finding tags.  Return a pair of dicts
+        (tags, tagtypes) where tags maps tag name to node, and tagtypes
+        maps tag name to a string like \'global\' or \'local\'.
+        Subclasses or extensions are free to add their own tags, but
+        should be aware that the returned dicts will be retained for the
+        duration of the localrepo object.'''
+
+        # XXX what tagtype should subclasses/extensions use?  Currently
+        # mq and bookmarks add tags, but do not set the tagtype at all.
+        # Should each extension invent its own tag type?  Should there
+        # be one tagtype for all such "virtual" tags?  Or is the status
+        # quo fine?
+
+        alltags = {}                    # map tag name to (node, hist)
         tagtypes = {}
 
-        def readtags(lines, fn, tagtype):
-            filetags = {}
-            count = 0
-
-            def warn(msg):
-                self.ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
-
-            for l in lines:
-                count += 1
-                if not l:
-                    continue
-                s = l.split(" ", 1)
-                if len(s) != 2:
-                    warn(_("cannot parse entry"))
-                    continue
-                node, key = s
-                key = encoding.tolocal(key.strip()) # stored in UTF-8
-                try:
-                    bin_n = bin(node)
-                except TypeError:
-                    warn(_("node '%s' is not well formed") % node)
-                    continue
-                if bin_n not in self.changelog.nodemap:
-                    # silently ignore as pull -r might cause this
-                    continue
-
-                h = []
-                if key in filetags:
-                    n, h = filetags[key]
-                    h.append(n)
-                filetags[key] = (bin_n, h)
-
-            for k, nh in filetags.iteritems():
-                if k not in globaltags:
-                    globaltags[k] = nh
-                    tagtypes[k] = tagtype
-                    continue
+        tags_.findglobaltags(self.ui, self, alltags, tagtypes)
+        tags_.readlocaltags(self.ui, self, alltags, tagtypes)
 
-                # we prefer the global tag if:
-                #  it supercedes us OR
-                #  mutual supercedes and it has a higher rank
-                # otherwise we win because we're tip-most
-                an, ah = nh
-                bn, bh = globaltags[k]
-                if (bn != an and an in bh and
-                    (bn not in ah or len(bh) > len(ah))):
-                    an = bn
-                ah.extend([n for n in bh if n not in ah])
-                globaltags[k] = an, ah
-                tagtypes[k] = tagtype
-
-        seen = set()
-        f = None
-        ctxs = []
-        for node in self.heads():
-            try:
-                fnode = self[node].filenode('.hgtags')
-            except error.LookupError:
-                continue
-            if fnode not in seen:
-                seen.add(fnode)
-                if not f:
-                    f = self.filectx('.hgtags', fileid=fnode)
-                else:
-                    f = f.filectx(fnode)
-                ctxs.append(f)
-
-        # read the tags file from each head, ending with the tip
-        for f in reversed(ctxs):
-            readtags(f.data().splitlines(), f, "global")
-
-        try:
-            data = encoding.fromlocal(self.opener("localtags").read())
-            # localtags are stored in the local character set
-            # while the internal tag table is stored in UTF-8
-            readtags(data.splitlines(), "localtags", "local")
-        except IOError:
-            pass
-
-        self.tagscache = {}
-        self._tagstypecache = {}
-        for k, nh in globaltags.iteritems():
-            n = nh[0]
-            if n != nullid:
-                self.tagscache[k] = n
-            self._tagstypecache[k] = tagtypes[k]
-        self.tagscache['tip'] = self.changelog.tip()
-        return self.tagscache
+        # Build the return dicts.  Have to re-encode tag names because
+        # the tags module always uses UTF-8 (in order not to lose info
+        # writing to the cache), but the rest of Mercurial wants them in
+        # local encoding.
+        tags = {}
+        for (name, (node, hist)) in alltags.iteritems():
+            if node != nullid:
+                tags[encoding.tolocal(name)] = node
+        tags['tip'] = self.changelog.tip()
+        tagtypes = dict([(encoding.tolocal(name), value)
+                         for (name, value) in tagtypes.iteritems()])
+        return (tags, tagtypes)
 
     def tagtype(self, tagname):
         '''
@@ -339,7 +289,7 @@
 
         self.tags()
 
-        return self._tagstypecache.get(tagname)
+        return self._tagtypes.get(tagname)
 
     def tagslist(self):
         '''return a list of tags ordered by revision'''
@@ -668,6 +618,7 @@
                                  % encoding.tolocal(self.dirstate.branch()))
                 self.invalidate()
                 self.dirstate.invalidate()
+                self.destroyed()
             else:
                 self.ui.warn(_("no rollback information available\n"))
         finally:
@@ -677,8 +628,8 @@
         for a in "changelog manifest".split():
             if a in self.__dict__:
                 delattr(self, a)
-        self.tagscache = None
-        self._tagstypecache = None
+        self._tags = None
+        self._tagtypes = None
         self.nodetagscache = None
         self.branchcache = None
         self._ubranchcache = None
@@ -966,6 +917,25 @@
             del tr
             lock.release()
 
+    def destroyed(self):
+        '''Inform the repository that nodes have been destroyed.
+        Intended for use by strip and rollback, so there's a common
+        place for anything that has to be done after destroying history.'''
+        # XXX it might be nice if we could take the list of destroyed
+        # nodes, but I don't see an easy way for rollback() to do that
+
+        # Ensure the persistent tag cache is updated.  Doing it now
+        # means that the tag cache only has to worry about destroyed
+        # heads immediately after a strip/rollback.  That in turn
+        # guarantees that "cachetip == currenttip" (comparing both rev
+        # and node) always means no nodes have been added or destroyed.
+
+        # XXX this is suboptimal when qrefresh'ing: we strip the current
+        # head, refresh the tag cache, then immediately add a new head.
+        # But I think doing it this way is necessary for the "instant
+        # tag cache retrieval" case to work.
+        tags_.findglobaltags(self.ui, self, {}, {})
+
     def walk(self, match, node=None):
         '''
         walk recursively through the directory tree or a given
--- a/mercurial/repair.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/mercurial/repair.py	Thu Jul 16 21:05:24 2009 -0700
@@ -142,3 +142,4 @@
         if backup != "strip":
             os.unlink(chgrpfile)
 
+    repo.destroyed()
--- a/mercurial/statichttprepo.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/mercurial/statichttprepo.py	Thu Jul 16 21:05:24 2009 -0700
@@ -114,7 +114,7 @@
 
         self.manifest = manifest.manifest(self.sopener)
         self.changelog = changelog.changelog(self.sopener)
-        self.tagscache = None
+        self._tags = None
         self.nodetagscache = None
         self.encodepats = None
         self.decodepats = None
--- a/mercurial/store.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/mercurial/store.py	Thu Jul 16 21:05:24 2009 -0700
@@ -284,16 +284,17 @@
         self.pathjoiner = pathjoiner
         self.path = self.pathjoiner(path, 'store')
         self.createmode = _calcmode(self.path)
-        self._op = opener(self.path)
-        self._op.createmode = self.createmode
-        self.fncache = fncache(self._op)
+        op = opener(self.path)
+        op.createmode = self.createmode
+        fnc = fncache(op)
+        self.fncache = fnc
 
         def fncacheopener(path, mode='r', *args, **kw):
             if (mode not in ('r', 'rb')
                 and path.startswith('data/')
-                and path not in self.fncache):
-                    self.fncache.add(path)
-            return self._op(hybridencode(path), mode, *args, **kw)
+                and path not in fnc):
+                    fnc.add(path)
+            return op(hybridencode(path), mode, *args, **kw)
         self.opener = fncacheopener
 
     def join(self, f):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/tags.py	Thu Jul 16 21:05:24 2009 -0700
@@ -0,0 +1,339 @@
+# tags.py - read tag info from local repository
+#
+# Copyright 2009 Matt Mackall <mpm@selenic.com>
+# Copyright 2009 Greg Ward <greg@gerg.ca>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+
+# Currently this module only deals with reading and caching tags.
+# Eventually, it could take care of updating (adding/removing/moving)
+# tags too.
+
+import os
+from node import nullid, bin, hex, short
+from i18n import _
+import encoding
+import error
+
+def _debugalways(ui, *msg):
+    ui.write(*msg)
+
+def _debugconditional(ui, *msg):
+    ui.debug(*msg)
+
+def _debugnever(ui, *msg):
+    pass
+
+_debug = _debugalways
+_debug = _debugnever
+
+def findglobaltags1(ui, repo, alltags, tagtypes):
+    '''Find global tags in repo by reading .hgtags from every head that
+    has a distinct version of it.  Updates the dicts alltags, tagtypes
+    in place: alltags maps tag name to (node, hist) pair (see _readtags()
+    below), and tagtypes maps tag name to tag type ('global' in this
+    case).'''
+
+    seen = set()
+    fctx = None
+    ctxs = []                       # list of filectx
+    for node in repo.heads():
+        try:
+            fnode = repo[node].filenode('.hgtags')
+        except error.LookupError:
+            continue
+        if fnode not in seen:
+            seen.add(fnode)
+            if not fctx:
+                fctx = repo.filectx('.hgtags', fileid=fnode)
+            else:
+                fctx = fctx.filectx(fnode)
+            ctxs.append(fctx)
+
+    # read the tags file from each head, ending with the tip
+    for fctx in reversed(ctxs):
+        filetags = _readtags(
+            ui, repo, fctx.data().splitlines(), fctx)
+        _updatetags(filetags, "global", alltags, tagtypes)
+
+def findglobaltags2(ui, repo, alltags, tagtypes):
+    '''Same as findglobaltags1(), but with caching.'''
+    # This is so we can be lazy and assume alltags contains only global
+    # tags when we pass it to _writetagcache().
+    assert len(alltags) == len(tagtypes) == 0, \
+           "findglobaltags() should be called first"
+
+    (heads, tagfnode, cachetags, shouldwrite) = _readtagcache(ui, repo)
+    if cachetags is not None:
+        assert not shouldwrite
+        # XXX is this really 100% correct?  are there oddball special
+        # cases where a global tag should outrank a local tag but won't,
+        # because cachetags does not contain rank info?
+        _updatetags(cachetags, 'global', alltags, tagtypes)
+        return
+
+    _debug(ui, "reading tags from %d head(s): %s\n"
+           % (len(heads), map(short, reversed(heads))))
+    seen = set()                    # set of fnode
+    fctx = None
+    for head in reversed(heads):        # oldest to newest
+        assert head in repo.changelog.nodemap, \
+               "tag cache returned bogus head %s" % short(head)
+
+        fnode = tagfnode.get(head)
+        if fnode and fnode not in seen:
+            seen.add(fnode)
+            if not fctx:
+                fctx = repo.filectx('.hgtags', fileid=fnode)
+            else:
+                fctx = fctx.filectx(fnode)
+
+            filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
+            _updatetags(filetags, 'global', alltags, tagtypes)
+
+    # and update the cache (if necessary)
+    if shouldwrite:
+        _writetagcache(ui, repo, heads, tagfnode, alltags)
+
+# Set this to findglobaltags1 to disable tag caching.
+findglobaltags = findglobaltags2
+
+def readlocaltags(ui, repo, alltags, tagtypes):
+    '''Read local tags in repo.  Update alltags and tagtypes.'''
+    try:
+        # localtags is in the local encoding; re-encode to UTF-8 on
+        # input for consistency with the rest of this module.
+        data = repo.opener("localtags").read()
+        filetags = _readtags(
+            ui, repo, data.splitlines(), "localtags",
+            recode=encoding.fromlocal)
+        _updatetags(filetags, "local", alltags, tagtypes)
+    except IOError:
+        pass
+
+def _readtags(ui, repo, lines, fn, recode=None):
+    '''Read tag definitions from a file (or any source of lines).
+    Return a mapping from tag name to (node, hist): node is the node id
+    from the last line read for that name, and hist is the list of node
+    ids previously associated with it (in file order).  All node ids are
+    binary, not hex.'''
+
+    filetags = {}               # map tag name to (node, hist)
+    count = 0
+
+    def warn(msg):
+        ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
+
+    for line in lines:
+        count += 1
+        if not line:
+            continue
+        try:
+            (nodehex, name) = line.split(" ", 1)
+        except ValueError:
+            warn(_("cannot parse entry"))
+            continue
+        name = name.strip()
+        if recode:
+            name = recode(name)
+        try:
+            nodebin = bin(nodehex)
+        except TypeError:
+            warn(_("node '%s' is not well formed") % nodehex)
+            continue
+        if nodebin not in repo.changelog.nodemap:
+            # silently ignore as pull -r might cause this
+            continue
+
+        # update filetags
+        hist = []
+        if name in filetags:
+            n, hist = filetags[name]
+            hist.append(n)
+        filetags[name] = (nodebin, hist)
+    return filetags
+
+def _updatetags(filetags, tagtype, alltags, tagtypes):
+    '''Incorporate the tag info read from one file into the two
+    dictionaries, alltags and tagtypes, that contain all tag
+    info (global across all heads plus local).'''
+
+    for name, nodehist in filetags.iteritems():
+        if name not in alltags:
+            alltags[name] = nodehist
+            tagtypes[name] = tagtype
+            continue
+
+        # we prefer alltags[name] if:
+        #  it supercedes us OR
+        #  mutual supercedes and it has a higher rank
+        # otherwise we win because we're tip-most
+        anode, ahist = nodehist
+        bnode, bhist = alltags[name]
+        if (bnode != anode and anode in bhist and
+            (bnode not in ahist or len(bhist) > len(ahist))):
+            anode = bnode
+        ahist.extend([n for n in bhist if n not in ahist])
+        alltags[name] = anode, ahist
+        tagtypes[name] = tagtype
+
+
+# The tag cache only stores info about heads, not the tag contents
+# from each head.  I.e. it doesn't try to squeeze out the maximum
+# performance, but is simpler has a better chance of actually
+# working correctly.  And this gives the biggest performance win: it
+# avoids looking up .hgtags in the manifest for every head, and it
+# can avoid calling heads() at all if there have been no changes to
+# the repo.
+
+def _readtagcache(ui, repo):
+    '''Read the tag cache and return a tuple (heads, fnodes, cachetags,
+    shouldwrite).  If the cache is completely up-to-date, cachetags is a
+    dict of the form returned by _readtags(); otherwise, it is None and
+    heads and fnodes are set.  In that case, heads is the list of all
+    heads currently in the repository (ordered from tip to oldest) and
+    fnodes is a mapping from head to .hgtags filenode.  If those two are
+    set, caller is responsible for reading tag info from each head.'''
+
+    try:
+        cachefile = repo.opener('tags.cache', 'r')
+        _debug(ui, 'reading tag cache from %s\n' % cachefile.name)
+    except IOError:
+        cachefile = None
+
+    # The cache file consists of lines like
+    #   <headrev> <headnode> [<tagnode>]
+    # where <headrev> and <headnode> redundantly identify a repository
+    # head from the time the cache was written, and <tagnode> is the
+    # filenode of .hgtags on that head.  Heads with no .hgtags file will
+    # have no <tagnode>.  The cache is ordered from tip to oldest (which
+    # is part of why <headrev> is there: a quick visual check is all
+    # that's required to ensure correct order).
+    # 
+    # This information is enough to let us avoid the most expensive part
+    # of finding global tags, which is looking up <tagnode> in the
+    # manifest for each head.
+    cacherevs = []                      # list of headrev
+    cacheheads = []                     # list of headnode
+    cachefnode = {}                     # map headnode to filenode
+    if cachefile:
+        for line in cachefile:
+            if line == "\n":
+                break
+            line = line.rstrip().split()
+            cacherevs.append(int(line[0]))
+            headnode = bin(line[1])
+            cacheheads.append(headnode)
+            if len(line) == 3:
+                fnode = bin(line[2])
+                cachefnode[headnode] = fnode
+
+    tipnode = repo.changelog.tip()
+    tiprev = len(repo.changelog) - 1
+
+    # Case 1 (common): tip is the same, so nothing has changed.
+    # (Unchanged tip trivially means no changesets have been added.
+    # But, thanks to localrepository.destroyed(), it also means none
+    # have been destroyed by strip or rollback.)
+    if cacheheads and cacheheads[0] == tipnode and cacherevs[0] == tiprev:
+        _debug(ui, "tag cache: tip unchanged\n")
+        tags = _readtags(ui, repo, cachefile, cachefile.name)
+        cachefile.close()
+        return (None, None, tags, False)
+    if cachefile:
+        cachefile.close()               # ignore rest of file
+        
+    repoheads = repo.heads()
+
+    # Case 2 (uncommon): empty repo; get out quickly and don't bother
+    # writing an empty cache.
+    if repoheads == [nullid]:
+        return ([], {}, {}, False)
+
+    # Case 3 (uncommon): cache file missing or empty.
+    if not cacheheads:
+        _debug(ui, 'tag cache: cache file missing or empty\n')
+
+    # Case 4 (uncommon): tip rev decreased.  This should only happen
+    # when we're called from localrepository.destroyed().  Refresh the
+    # cache so future invocations will not see disappeared heads in the
+    # cache.
+    elif cacheheads and tiprev < cacherevs[0]:
+        _debug(ui,
+               'tag cache: tip rev decremented (from %d to %d), '
+               'so we must be destroying nodes\n'
+               % (cacherevs[0], tiprev))
+
+    # Case 5 (common): tip has changed, so we've added/replaced heads.
+    else:
+        _debug(ui,
+               'tag cache: tip has changed (%d:%s); must find new heads\n'
+               % (tiprev, short(tipnode)))
+
+    # Luckily, the code to handle cases 3, 4, 5 is the same.  So the
+    # above if/elif/else can disappear once we're confident this thing
+    # actually works and we don't need the debug output.
+
+    # N.B. in case 4 (nodes destroyed), "new head" really means "newly
+    # exposed".
+    newheads = [head
+                for head in repoheads
+                if head not in set(cacheheads)]
+    _debug(ui, 'tag cache: found %d head(s) not in cache: %s\n'
+           % (len(newheads), map(short, newheads)))
+
+    # Now we have to lookup the .hgtags filenode for every new head.
+    # This is the most expensive part of finding tags, so performance
+    # depends primarily on the size of newheads.  Worst case: no cache
+    # file, so newheads == repoheads.
+    for head in newheads:
+        cctx = repo[head]
+        try:
+            fnode = cctx.filenode('.hgtags')
+            cachefnode[head] = fnode
+        except error.LookupError:
+            # no .hgtags file on this head
+            pass
+
+    # Caller has to iterate over all heads, but can use the filenodes in
+    # cachefnode to get to each .hgtags revision quickly.
+    return (repoheads, cachefnode, None, True)
+
+def _writetagcache(ui, repo, heads, tagfnode, cachetags):
+
+    cachefile = repo.opener('tags.cache', 'w', atomictemp=True)
+    _debug(ui, 'writing cache file %s\n' % cachefile.name)
+
+    realheads = repo.heads()            # for sanity checks below
+    for head in heads:
+        # temporary sanity checks; these can probably be removed
+        # once this code has been in crew for a few weeks
+        assert head in repo.changelog.nodemap, \
+               'trying to write non-existent node %s to tag cache' % short(head)
+        assert head in realheads, \
+               'trying to write non-head %s to tag cache' % short(head)
+        assert head != nullid, \
+               'trying to write nullid to tag cache'
+
+        # This can't fail because of the first assert above.  When/if we
+        # remove that assert, we might want to catch LookupError here
+        # and downgrade it to a warning.
+        rev = repo.changelog.rev(head)
+
+        fnode = tagfnode.get(head)
+        if fnode:
+            cachefile.write('%d %s %s\n' % (rev, hex(head), hex(fnode)))
+        else:
+            cachefile.write('%d %s\n' % (rev, hex(head)))
+
+    # Tag names in the cache are in UTF-8 -- which is the whole reason
+    # we keep them in UTF-8 throughout this module.  If we converted
+    # them local encoding on input, we would lose info writing them to
+    # the cache.
+    cachefile.write('\n')
+    for (name, (node, hist)) in cachetags.iteritems():
+        cachefile.write("%s %s\n" % (hex(node), name))
+
+    cachefile.rename()
+    cachefile.close()
--- a/mercurial/ui.py	Thu Jul 16 23:25:26 2009 +0200
+++ b/mercurial/ui.py	Thu Jul 16 21:05:24 2009 -0700
@@ -349,3 +349,33 @@
                 self.config("ui", "editor") or
                 os.environ.get("VISUAL") or
                 os.environ.get("EDITOR", "vi"))
+
+    def progress(self, topic, pos, item="", unit="", total=None):
+        '''show a progress message
+
+        With stock hg, this is simply a debug message that is hidden
+        by default, but with extensions or GUI tools it may be
+        visible. 'topic' is the current operation, 'item' is a
+        non-numeric marker of the current position (ie the currently
+        in-process file), 'pos' is the current numeric position (ie
+        revision, bytes, etc.), units is a corresponding unit label,
+        and total is the highest expected pos.
+
+        Multiple nested topics may be active at a time. All topics
+        should be marked closed by setting pos to None at termination.
+        '''
+
+        if pos == None or not self.debugflag:
+            return
+
+        if units:
+            units = ' ' + units
+        if item:
+            item = ' ' + item
+
+        if total:
+            pct = 100.0 * pos / total
+            ui.debug('%s:%s %s/%s%s (%4.2g%%)\n'
+                     % (topic, item, pos, total, units, pct))
+        else:
+            ui.debug('%s:%s %s%s\n' % (topic, item, pos, units))
--- a/tests/test-mq	Thu Jul 16 23:25:26 2009 +0200
+++ b/tests/test-mq	Thu Jul 16 21:05:24 2009 -0700
@@ -107,9 +107,18 @@
 hg qpop
 checkundo qpop
 
-echo % qpush
+echo % qpush with dump of tag cache
 
+# Dump the tag cache to ensure that it has exactly one head after qpush.
+rm -f .hg/tags.cache
+hg tags > /dev/null
+echo ".hg/tags.cache (pre qpush):"
+sed 's/ [0-9a-f]*//' .hg/tags.cache
 hg qpush
+hg tags > /dev/null
+echo ".hg/tags.cache (post qpush):"
+sed 's/ [0-9a-f]*//' .hg/tags.cache
+
 checkundo qpush
 
 cd ..
--- a/tests/test-mq.out	Thu Jul 16 23:25:26 2009 +0200
+++ b/tests/test-mq.out	Thu Jul 16 21:05:24 2009 -0700
@@ -110,9 +110,15 @@
 % qpop
 popping test.patch
 patch queue now empty
-% qpush
+% qpush with dump of tag cache
+.hg/tags.cache (pre qpush):
+1
+
 applying test.patch
 now at: test.patch
+.hg/tags.cache (post qpush):
+2
+
 % pop/push outside repo
 popping test.patch
 patch queue now empty
--- a/tests/test-tags	Thu Jul 16 23:25:26 2009 +0200
+++ b/tests/test-tags	Thu Jul 16 21:05:24 2009 -0700
@@ -1,24 +1,46 @@
 #!/bin/sh
 
+cacheexists() {
+    [ -f .hg/tags.cache ] && echo "tag cache exists" || echo "no tag cache"
+}
+
+# XXX need to test that the tag cache works when we strip an old head
+# and add a new one rooted off non-tip: i.e. node and rev of tip are the
+# same, but stuff has changed behind tip.
+
+echo "% setup"
 mkdir t
 cd t
 hg init
+cacheexists
 hg id
+cacheexists
 echo a > a
 hg add a
-hg commit -m "test" -d "1000000 0"
+hg commit -m "test"
 hg co
 hg identify
-T=`hg tip --debug | head -n 1 | cut -d : -f 3`
+cacheexists
+
+echo "% create local tag with long name"
+T=`hg identify --debug --id`
 hg tag -l "This is a local tag with a really long name!"
 hg tags
 rm .hg/localtags
+
+echo "% create a tag behind hg's back"
 echo "$T first" > .hgtags
 cat .hgtags
 hg add .hgtags
-hg commit -m "add tags" -d "1000000 0"
+hg commit -m "add tags"
 hg tags
 hg identify
+
+# repeat with cold tag cache
+rm -f .hg/tags.cache
+hg identify
+
+echo "% create a branch"
 echo bb > a
 hg status
 hg identify
@@ -28,89 +50,126 @@
 hg status
 echo 1 > b
 hg add b
-hg commit -m "branch" -d "1000000 0"
+hg commit -m "branch"
 hg id
+
+echo "% merge the two heads"
 hg merge 1
 hg id
 hg status
 
-hg commit -m "merge" -d "1000000 0"
+hg commit -m "merge"
 
-# create fake head, make sure tag not visible afterwards
+echo "% create fake head, make sure tag not visible afterwards"
 cp .hgtags tags
-hg tag -d "1000000 0" last
+hg tag last
 hg rm .hgtags
-hg commit -m "remove" -d "1000000 0"
+hg commit -m "remove"
 
 mv tags .hgtags
 hg add .hgtags
-hg commit -m "readd" -d "1000000 0"
+hg commit -m "readd"
 
 hg tags
 
-# invalid tags
+echo "% add invalid tags"
 echo "spam" >> .hgtags
 echo >> .hgtags
 echo "foo bar" >> .hgtags
 echo "$T invalid" | sed "s/..../a5a5/" >> .hg/localtags
-hg commit -m "tags" -d "1000000 0"
+echo "committing .hgtags:"
+cat .hgtags 
+hg commit -m "tags"
 
-# report tag parse error on other head
+echo "% report tag parse error on other head"
 hg up 3
 echo 'x y' >> .hgtags
-hg commit -m "head" -d "1000000 0"
+hg commit -m "head"
 
 hg tags
 hg tip
 
-# test tag precedence rules
+echo "% test tag precedence rules"
 cd ..
 hg init t2
 cd t2
 echo foo > foo
 hg add foo
-hg ci -m 'add foo' -d '1000000 0'      # rev 0
-hg tag -d '1000000 0' bar              # rev 1
+hg ci -m 'add foo'      # rev 0
+hg tag bar              # rev 1
 echo >> foo
-hg ci -m 'change foo 1' -d '1000000 0' # rev 2
+hg ci -m 'change foo 1' # rev 2
 hg up -C 1
-hg tag -r 1 -d '1000000 0' -f bar      # rev 3
+hg tag -r 1 -f bar      # rev 3
 hg up -C 1
 echo >> foo
-hg ci -m 'change foo 2' -d '1000000 0' # rev 4
+hg ci -m 'change foo 2' # rev 4
+hg tags
+hg tags         # repeat in case of cache effects
+
+dumptags() {
+    rev=$1
+    echo "rev $rev: .hgtags:"
+    hg cat -r$rev .hgtags
+}
+
+echo "% detailed dump of tag info"
+echo "heads:"
+hg heads -q             # expect 4, 3, 2
+dumptags 2
+dumptags 3
+dumptags 4
+echo ".hg/tags.cache:"
+[ -f .hg/tags.cache ] && cat .hg/tags.cache || echo "no such file"
+
+echo "% test tag removal"
+hg tag --remove bar     # rev 5
+hg tip -vp
+hg tags
+hg tags                 # again, try to expose cache bugs
+
+echo '% remove nonexistent tag'
+hg tag --remove foobar
+hg tip
+
+echo "% rollback undoes tag operation"
+hg rollback             # destroy rev 5 (restore bar)
+hg tags
 hg tags
 
-# test tag removal
-hg tag --remove -d '1000000 0' bar
-hg tip
-hg tags
-
-echo '% remove nonexistent tag'
-hg tag --remove -d '1000000 0' foobar
-hg tip
-
-# test tag rank
+echo "% test tag rank"
 cd ..
 hg init t3
 cd t3
 echo foo > foo
 hg add foo
-hg ci -m 'add foo' -d '1000000 0'       # rev 0
-hg tag -d '1000000 0' -f bar            # rev 1 bar -> 0
-hg tag -d '1000000 0' -f bar            # rev 2 bar -> 1
-hg tag -d '1000000 0' -fr 0 bar         # rev 3 bar -> 0
-hg tag -d '1000000 0' -fr 1 bar         # rev 3 bar -> 1
-hg tag -d '1000000 0' -fr 0 bar         # rev 4 bar -> 0
+hg ci -m 'add foo'       # rev 0
+hg tag -f bar            # rev 1 bar -> 0
+hg tag -f bar            # rev 2 bar -> 1
+hg tag -fr 0 bar         # rev 3 bar -> 0
+hg tag -fr 1 bar         # rev 4 bar -> 1
+hg tag -fr 0 bar         # rev 5 bar -> 0
 hg tags
 hg co 3
 echo barbar > foo
-hg ci -m 'change foo' -d '1000000 0'    # rev 0
+hg ci -m 'change foo'    # rev 6
+hg tags
+
+echo "% don't allow moving tag without -f"
+hg tag -r 3 bar
 hg tags
 
-hg tag -d '1000000 0' -r 3 bar         # should complain
-hg tags
+echo "% strip 1: expose an old head"
+hg --config extensions.mq= strip 5 > /dev/null 2>&1
+hg tags                  # partly stale cache
+hg tags                  # up-to-date cache
+echo "% strip 2: destroy whole branch, no old head exposed"
+hg --config extensions.mq= strip 4 > /dev/null 2>&1 
+hg tags                  # partly stale
+rm -f .hg/tags.cache
+hg tags                  # cold cache
 
-# test tag rank with 3 heads
+echo "% test tag rank with 3 heads"
 cd ..
 hg init t4
 cd t4
@@ -124,10 +183,11 @@
 hg tags
 hg up -qC 0
 hg tag -m 'retag rev 0' -fr 0 bar  # rev 4 bar -> 0, but bar stays at 2
-echo % bar should still point to rev 2
+echo "% bar should still point to rev 2"
 hg tags
 
 
+echo "% remove local as global and global as local"
 # test that removing global/local tags does not get confused when trying
 # to remove a tag of type X which actually only exists as a type Y
 cd ..
--- a/tests/test-tags.out	Thu Jul 16 23:25:26 2009 +0200
+++ b/tests/test-tags.out	Thu Jul 16 21:05:24 2009 -0700
@@ -1,78 +1,147 @@
+% setup
+no tag cache
 000000000000 tip
+no tag cache
 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-0acdaf898367 tip
-tip                                0:0acdaf898367
-This is a local tag with a really long name!     0:0acdaf898367
-0acdaf8983679e0aac16e811534eb49d7ee1f2b4 first
-tip                                1:8a3ca90d111d
-first                              0:0acdaf898367
-8a3ca90d111d tip
+acb14030fe0a tip
+tag cache exists
+% create local tag with long name
+tip                                0:acb14030fe0a
+This is a local tag with a really long name!     0:acb14030fe0a
+% create a tag behind hg's back
+acb14030fe0a21b60322c440ad2d20cf7685a376 first
+tip                                1:b9154636be93
+first                              0:acb14030fe0a
+b9154636be93 tip
+b9154636be93 tip
+% create a branch
 M a
-8a3ca90d111d+ tip
+b9154636be93+ tip
 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
-0acdaf898367+ first
-0acdaf898367+ first
+acb14030fe0a+ first
+acb14030fe0a+ first
 M a
 created new head
-8216907a933d tip
+c8edf04160c7 tip
+% merge the two heads
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 (branch merge, don't forget to commit)
-8216907a933d+8a3ca90d111d+ tip
+c8edf04160c7+b9154636be93+ tip
 M .hgtags
-tip                                6:e2174d339386
-first                              0:0acdaf898367
+% create fake head, make sure tag not visible afterwards
+tip                                6:35ff301afafe
+first                              0:acb14030fe0a
+% add invalid tags
+committing .hgtags:
+acb14030fe0a21b60322c440ad2d20cf7685a376 first
+spam
+
+foo bar
+% report tag parse error on other head
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 created new head
-.hgtags@c071f74ab5eb, line 2: cannot parse entry
-.hgtags@c071f74ab5eb, line 4: node 'foo' is not well formed
-.hgtags@4ca6f1b1a68c, line 2: node 'x' is not well formed
-tip                                8:4ca6f1b1a68c
-first                              0:0acdaf898367
-changeset:   8:4ca6f1b1a68c
-.hgtags@c071f74ab5eb, line 2: cannot parse entry
-.hgtags@c071f74ab5eb, line 4: node 'foo' is not well formed
-.hgtags@4ca6f1b1a68c, line 2: node 'x' is not well formed
+.hgtags@75d9f02dfe28, line 2: cannot parse entry
+.hgtags@75d9f02dfe28, line 4: node 'foo' is not well formed
+.hgtags@c4be69a18c11, line 2: node 'x' is not well formed
+tip                                8:c4be69a18c11
+first                              0:acb14030fe0a
+changeset:   8:c4be69a18c11
 tag:         tip
-parent:      3:b2ef3841386b
+parent:      3:ac5e980c4dc0
 user:        test
-date:        Mon Jan 12 13:46:40 1970 +0000
+date:        Thu Jan 01 00:00:00 1970 +0000
 summary:     head
 
+% test tag precedence rules
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 created new head
-tip                                4:36195b728445
-bar                                1:b204a97e6e8d
-changeset:   5:1f98c77278de
+tip                                4:0c192d7d5e6b
+bar                                1:78391a272241
+tip                                4:0c192d7d5e6b
+bar                                1:78391a272241
+% detailed dump of tag info
+heads:
+4:0c192d7d5e6b
+3:6fa450212aeb
+2:7a94127795a3
+rev 2: .hgtags:
+bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
+rev 3: .hgtags:
+bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
+bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
+78391a272241d70354aa14c874552cad6b51bb42 bar
+rev 4: .hgtags:
+bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
+.hg/tags.cache:
+4 0c192d7d5e6b78a714de54a2e9627952a877e25a 0c04f2a8af31de17fab7422878ee5a2dadbc943d
+3 6fa450212aeb2a21ed616a54aea39a4a27894cd7 7d3b718c964ef37b89e550ebdafd5789e76ce1b0
+2 7a94127795a33c10a370c93f731fd9fea0b79af6 0c04f2a8af31de17fab7422878ee5a2dadbc943d
+
+78391a272241d70354aa14c874552cad6b51bb42 bar
+% test tag removal
+changeset:   5:5f6e8655b1c7
 tag:         tip
 user:        test
-date:        Mon Jan 12 13:46:40 1970 +0000
-summary:     Removed tag bar
+date:        Thu Jan 01 00:00:00 1970 +0000
+files:       .hgtags
+description:
+Removed tag bar
+
 
-tip                                5:1f98c77278de
+diff -r 0c192d7d5e6b -r 5f6e8655b1c7 .hgtags
+--- a/.hgtags	Thu Jan 01 00:00:00 1970 +0000
++++ b/.hgtags	Thu Jan 01 00:00:00 1970 +0000
+@@ -1,1 +1,3 @@
+ bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
++78391a272241d70354aa14c874552cad6b51bb42 bar
++0000000000000000000000000000000000000000 bar
+
+tip                                5:5f6e8655b1c7
+tip                                5:5f6e8655b1c7
 % remove nonexistent tag
 abort: tag 'foobar' does not exist
-changeset:   5:1f98c77278de
+changeset:   5:5f6e8655b1c7
 tag:         tip
 user:        test
-date:        Mon Jan 12 13:46:40 1970 +0000
+date:        Thu Jan 01 00:00:00 1970 +0000
 summary:     Removed tag bar
 
-tip                                5:e86d7ed95fd3
-bar                                0:b409d9da318e
+% rollback undoes tag operation
+rolling back last transaction
+tip                                4:0c192d7d5e6b
+bar                                1:78391a272241
+tip                                4:0c192d7d5e6b
+bar                                1:78391a272241
+% test tag rank
+tip                                5:85f05169d91d
+bar                                0:bbd179dfa0a7
 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 created new head
-tip                                6:b744fbe1f6dd
-bar                                0:b409d9da318e
+tip                                6:735c3ca72986
+bar                                0:bbd179dfa0a7
+% don't allow moving tag without -f
 abort: tag 'bar' already exists (use -f to force)
-tip                                6:b744fbe1f6dd
-bar                                0:b409d9da318e
+tip                                6:735c3ca72986
+bar                                0:bbd179dfa0a7
+% strip 1: expose an old head
+tip                                5:735c3ca72986
+bar                                1:78391a272241
+tip                                5:735c3ca72986
+bar                                1:78391a272241
+% strip 2: destroy whole branch, no old head exposed
+tip                                4:735c3ca72986
+bar                                0:bbd179dfa0a7
+tip                                4:735c3ca72986
+bar                                0:bbd179dfa0a7
+% test tag rank with 3 heads
 adding foo
 tip                                3:197c21bbbf2c
 bar                                2:6fa450212aeb
 % bar should still point to rev 2
 tip                                4:3b4b14ed0202
 bar                                2:6fa450212aeb
+% remove local as global and global as local
 adding foo
 abort: tag 'localtag' is not a global tag
 abort: tag 'globaltag' is not a local tag