Merge with hg

This commit is contained in:
Brendan Cully 2009-07-16 21:05:24 -07:00
commit 09920a4acd
14 changed files with 713 additions and 207 deletions

View File

@ -51,7 +51,7 @@ def perftags(ui, repo):
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)

View File

@ -293,14 +293,11 @@ def reposetup(ui, repo):
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

View File

@ -2415,34 +2415,33 @@ def reposetup(ui, repo):
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

View File

@ -49,6 +49,9 @@ def decode(arg):
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 @@ def encode(arg):
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 @@ def reposetup(ui, repo):
# 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)

View File

@ -13,6 +13,7 @@ import lock, transaction, store, encoding
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 @@ class localrepository(repo.repository):
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 @@ class localrepository(repo.repository):
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 @@ class localrepository(repo.repository):
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()
globaltags = {}
return self._tags
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
tags_.findglobaltags(self.ui, self, alltags, tagtypes)
tags_.readlocaltags(self.ui, self, alltags, tagtypes)
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
# 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 @@ class localrepository(repo.repository):
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 @@ class localrepository(repo.repository):
% 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 @@ class localrepository(repo.repository):
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 @@ class localrepository(repo.repository):
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

View File

@ -142,3 +142,4 @@ def strip(ui, repo, node, backup="all"):
if backup != "strip":
os.unlink(chgrpfile)
repo.destroyed()

View File

@ -114,7 +114,7 @@ class statichttprepository(localrepo.localrepository):
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

View File

@ -284,16 +284,17 @@ class fncachestore(basicstore):
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):

339
mercurial/tags.py Normal file
View File

@ -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()

View File

@ -349,3 +349,33 @@ class ui(object):
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))

View File

@ -107,9 +107,18 @@ echo % qpop
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 ..

View File

@ -110,9 +110,15 @@ working dir diff:
% 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

View File

@ -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 -v id
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
# test tag removal
hg tag --remove -d '1000000 0' bar
hg tip
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 -d '1000000 0' foobar
hg tag --remove foobar
hg tip
# test tag rank
echo "% rollback undoes tag operation"
hg rollback # destroy rev 5 (restore bar)
hg tags
hg tags
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
hg tag -d '1000000 0' -r 3 bar # should complain
echo "% don't allow moving tag without -f"
hg tag -r 3 bar
hg tags
# test tag rank with 3 heads
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
echo "% test tag rank with 3 heads"
cd ..
hg init t4
cd t4
@ -124,10 +183,11 @@ hg tag -fr 2 bar # rev 3 bar -> 2
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 ..

View File

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