# HG changeset patch # User Brendan Cully # Date 1247803524 25200 # Node ID b67adc2daa159d03582783a1cd81e11dba0c9c02 # Parent 2399362b3bb03691523dd1c2f3ab6f797eaf4c4e# Parent 47ce7a3a1fb0364a15ab7217831e0cb847cf178e Merge with hg diff -r 2399362b3bb0 -r b67adc2daa15 contrib/perf.py --- 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) diff -r 2399362b3bb0 -r b67adc2daa15 hgext/bookmarks.py --- 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 diff -r 2399362b3bb0 -r b67adc2daa15 hgext/mq.py --- 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 diff -r 2399362b3bb0 -r b67adc2daa15 hgext/win32mbcs.py --- 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) diff -r 2399362b3bb0 -r b67adc2daa15 mercurial/localrepo.py --- 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 diff -r 2399362b3bb0 -r b67adc2daa15 mercurial/repair.py --- 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() diff -r 2399362b3bb0 -r b67adc2daa15 mercurial/statichttprepo.py --- 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 diff -r 2399362b3bb0 -r b67adc2daa15 mercurial/store.py --- 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): diff -r 2399362b3bb0 -r b67adc2daa15 mercurial/tags.py --- /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 +# Copyright 2009 Greg Ward +# +# 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 + # [] + # where and redundantly identify a repository + # head from the time the cache was written, and is the + # filenode of .hgtags on that head. Heads with no .hgtags file will + # have no . The cache is ordered from tip to oldest (which + # is part of why 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 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() diff -r 2399362b3bb0 -r b67adc2daa15 mercurial/ui.py --- 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)) diff -r 2399362b3bb0 -r b67adc2daa15 tests/test-mq --- 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 .. diff -r 2399362b3bb0 -r b67adc2daa15 tests/test-mq.out --- 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 diff -r 2399362b3bb0 -r b67adc2daa15 tests/test-tags --- 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 .. diff -r 2399362b3bb0 -r b67adc2daa15 tests/test-tags.out --- 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