tags.py
297 lines
| 11.2 KiB
| text/x-python
|
PythonLexer
/ mercurial / tags.py
|
r9149 | # 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 | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r9149 | |||
|
r9151 | # Currently this module only deals with reading and caching tags. | ||
# Eventually, it could take care of updating (adding/removing/moving) | ||||
# tags too. | ||||
|
r9149 | |||
|
r9151 | from node import nullid, bin, hex, short | ||
|
r9149 | from i18n import _ | ||
import encoding | ||||
import error | ||||
|
r14038 | import errno | ||
|
r9149 | |||
|
r11351 | def findglobaltags(ui, repo, alltags, tagtypes): | ||
|
r9149 | '''Find global tags in repo by reading .hgtags from every head that | ||
|
r11351 | has a distinct version of it, using a cache to avoid excess work. | ||
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).''' | ||||
|
r9152 | # 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 | ||||
|
r9151 | |||
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: | ||||
|
r9152 | _writetagcache(ui, repo, heads, tagfnode, alltags) | ||
|
r9151 | |||
|
r9149 | def readlocaltags(ui, repo, alltags, tagtypes): | ||
'''Read local tags in repo. Update alltags and tagtypes.''' | ||||
try: | ||||
|
r14168 | data = repo.opener.read("localtags") | ||
|
r14038 | except IOError, inst: | ||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
return | ||||
# localtags is in the local encoding; re-encode to UTF-8 on | ||||
# input for consistency with the rest of this module. | ||||
filetags = _readtags( | ||||
ui, repo, data.splitlines(), "localtags", | ||||
recode=encoding.fromlocal) | ||||
_updatetags(filetags, "local", alltags, tagtypes) | ||||
|
r9149 | |||
|
r9152 | def _readtags(ui, repo, lines, fn, recode=None): | ||
|
r9149 | '''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 | ||||
|
r9152 | name = name.strip() | ||
if recode: | ||||
name = recode(name) | ||||
|
r9149 | try: | ||
nodebin = bin(nodehex) | ||||
except TypeError: | ||||
warn(_("node '%s' is not well formed") % nodehex) | ||||
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 | ||||
|
r9151 | |||
# 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): | ||||
|
r9152 | '''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.''' | ||||
|
r9151 | |||
try: | ||||
|
r13341 | cachefile = repo.opener('cache/tags', 'r') | ||
|
r11066 | # force reading the file for static-http | ||
cachelines = iter(cachefile) | ||||
|
r9151 | 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). | ||||
|
r9312 | # | ||
|
r9151 | # 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: | ||||
|
r12758 | try: | ||
for line in cachelines: | ||||
if line == "\n": | ||||
break | ||||
|
r16589 | line = line.split() | ||
|
r12758 | cacherevs.append(int(line[0])) | ||
headnode = bin(line[1]) | ||||
cacheheads.append(headnode) | ||||
if len(line) == 3: | ||||
fnode = bin(line[2]) | ||||
cachefnode[headnode] = fnode | ||||
|
r14020 | except Exception: | ||
|
r13272 | # corruption of the tags cache, just recompute it | ||
ui.warn(_('.hg/cache/tags is corrupt, rebuilding it\n')) | ||||
|
r12758 | cacheheads = [] | ||
cacherevs = [] | ||||
cachefnode = {} | ||||
|
r9151 | |||
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: | ||||
|
r11066 | tags = _readtags(ui, repo, cachelines, cachefile.name) | ||
|
r9152 | cachefile.close() | ||
return (None, None, tags, False) | ||||
if cachefile: | ||||
cachefile.close() # ignore rest of file | ||||
|
r9312 | |||
|
r9151 | repoheads = repo.heads() | ||
# Case 2 (uncommon): empty repo; get out quickly and don't bother | ||||
# writing an empty cache. | ||||
if repoheads == [nullid]: | ||||
|
r9152 | return ([], {}, {}, False) | ||
|
r9151 | |||
# Case 3 (uncommon): cache file missing or empty. | ||||
# 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. | ||||
# Case 5 (common): tip has changed, so we've added/replaced heads. | ||||
|
r11352 | # As it happens, the code to handle cases 3, 4, 5 is the same. | ||
|
r9151 | |||
# N.B. in case 4 (nodes destroyed), "new head" really means "newly | ||||
# exposed". | ||||
|
r16730 | if not len(repo.file('.hgtags')): | ||
# No tags have ever been committed, so we can avoid a | ||||
# potentially expensive search. | ||||
return (repoheads, cachefnode, None, True) | ||||
|
r9151 | newheads = [head | ||
for head in repoheads | ||||
if head not in set(cacheheads)] | ||||
# 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. | ||||
|
r9152 | return (repoheads, cachefnode, None, True) | ||
|
r9151 | |||
|
r9152 | def _writetagcache(ui, repo, heads, tagfnode, cachetags): | ||
|
r9151 | |||
|
r9366 | try: | ||
|
r13341 | cachefile = repo.opener('cache/tags', 'w', atomictemp=True) | ||
|
r9366 | except (OSError, IOError): | ||
return | ||||
|
r9151 | |||
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))) | ||||
|
r9152 | # 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)) | ||||
|
r14662 | try: | ||
|
r15057 | cachefile.close() | ||
|
r14662 | except (OSError, IOError): | ||
pass | ||||