--- 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