eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/mercurial/tags.py
changeset 69 c6bca38c1cbf
equal deleted inserted replaced
68:5ff1fc726848 69:c6bca38c1cbf
       
     1 # tags.py - read tag info from local repository
       
     2 #
       
     3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
       
     4 # Copyright 2009 Greg Ward <greg@gerg.ca>
       
     5 #
       
     6 # This software may be used and distributed according to the terms of the
       
     7 # GNU General Public License version 2 or any later version.
       
     8 
       
     9 # Currently this module only deals with reading and caching tags.
       
    10 # Eventually, it could take care of updating (adding/removing/moving)
       
    11 # tags too.
       
    12 
       
    13 from node import nullid, bin, hex, short
       
    14 from i18n import _
       
    15 import encoding
       
    16 import error
       
    17 
       
    18 def findglobaltags(ui, repo, alltags, tagtypes):
       
    19     '''Find global tags in repo by reading .hgtags from every head that
       
    20     has a distinct version of it, using a cache to avoid excess work.
       
    21     Updates the dicts alltags, tagtypes in place: alltags maps tag name
       
    22     to (node, hist) pair (see _readtags() below), and tagtypes maps tag
       
    23     name to tag type ("global" in this case).'''
       
    24     # This is so we can be lazy and assume alltags contains only global
       
    25     # tags when we pass it to _writetagcache().
       
    26     assert len(alltags) == len(tagtypes) == 0, \
       
    27            "findglobaltags() should be called first"
       
    28 
       
    29     (heads, tagfnode, cachetags, shouldwrite) = _readtagcache(ui, repo)
       
    30     if cachetags is not None:
       
    31         assert not shouldwrite
       
    32         # XXX is this really 100% correct?  are there oddball special
       
    33         # cases where a global tag should outrank a local tag but won't,
       
    34         # because cachetags does not contain rank info?
       
    35         _updatetags(cachetags, 'global', alltags, tagtypes)
       
    36         return
       
    37 
       
    38     seen = set()                    # set of fnode
       
    39     fctx = None
       
    40     for head in reversed(heads):        # oldest to newest
       
    41         assert head in repo.changelog.nodemap, \
       
    42                "tag cache returned bogus head %s" % short(head)
       
    43 
       
    44         fnode = tagfnode.get(head)
       
    45         if fnode and fnode not in seen:
       
    46             seen.add(fnode)
       
    47             if not fctx:
       
    48                 fctx = repo.filectx('.hgtags', fileid=fnode)
       
    49             else:
       
    50                 fctx = fctx.filectx(fnode)
       
    51 
       
    52             filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
       
    53             _updatetags(filetags, 'global', alltags, tagtypes)
       
    54 
       
    55     # and update the cache (if necessary)
       
    56     if shouldwrite:
       
    57         _writetagcache(ui, repo, heads, tagfnode, alltags)
       
    58 
       
    59 def readlocaltags(ui, repo, alltags, tagtypes):
       
    60     '''Read local tags in repo.  Update alltags and tagtypes.'''
       
    61     try:
       
    62         # localtags is in the local encoding; re-encode to UTF-8 on
       
    63         # input for consistency with the rest of this module.
       
    64         data = repo.opener("localtags").read()
       
    65         filetags = _readtags(
       
    66             ui, repo, data.splitlines(), "localtags",
       
    67             recode=encoding.fromlocal)
       
    68         _updatetags(filetags, "local", alltags, tagtypes)
       
    69     except IOError:
       
    70         pass
       
    71 
       
    72 def _readtags(ui, repo, lines, fn, recode=None):
       
    73     '''Read tag definitions from a file (or any source of lines).
       
    74     Return a mapping from tag name to (node, hist): node is the node id
       
    75     from the last line read for that name, and hist is the list of node
       
    76     ids previously associated with it (in file order).  All node ids are
       
    77     binary, not hex.'''
       
    78 
       
    79     filetags = {}               # map tag name to (node, hist)
       
    80     count = 0
       
    81 
       
    82     def warn(msg):
       
    83         ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
       
    84 
       
    85     for line in lines:
       
    86         count += 1
       
    87         if not line:
       
    88             continue
       
    89         try:
       
    90             (nodehex, name) = line.split(" ", 1)
       
    91         except ValueError:
       
    92             warn(_("cannot parse entry"))
       
    93             continue
       
    94         name = name.strip()
       
    95         if recode:
       
    96             name = recode(name)
       
    97         try:
       
    98             nodebin = bin(nodehex)
       
    99         except TypeError:
       
   100             warn(_("node '%s' is not well formed") % nodehex)
       
   101             continue
       
   102         if nodebin not in repo.changelog.nodemap:
       
   103             # silently ignore as pull -r might cause this
       
   104             continue
       
   105 
       
   106         # update filetags
       
   107         hist = []
       
   108         if name in filetags:
       
   109             n, hist = filetags[name]
       
   110             hist.append(n)
       
   111         filetags[name] = (nodebin, hist)
       
   112     return filetags
       
   113 
       
   114 def _updatetags(filetags, tagtype, alltags, tagtypes):
       
   115     '''Incorporate the tag info read from one file into the two
       
   116     dictionaries, alltags and tagtypes, that contain all tag
       
   117     info (global across all heads plus local).'''
       
   118 
       
   119     for name, nodehist in filetags.iteritems():
       
   120         if name not in alltags:
       
   121             alltags[name] = nodehist
       
   122             tagtypes[name] = tagtype
       
   123             continue
       
   124 
       
   125         # we prefer alltags[name] if:
       
   126         #  it supercedes us OR
       
   127         #  mutual supercedes and it has a higher rank
       
   128         # otherwise we win because we're tip-most
       
   129         anode, ahist = nodehist
       
   130         bnode, bhist = alltags[name]
       
   131         if (bnode != anode and anode in bhist and
       
   132             (bnode not in ahist or len(bhist) > len(ahist))):
       
   133             anode = bnode
       
   134         ahist.extend([n for n in bhist if n not in ahist])
       
   135         alltags[name] = anode, ahist
       
   136         tagtypes[name] = tagtype
       
   137 
       
   138 
       
   139 # The tag cache only stores info about heads, not the tag contents
       
   140 # from each head.  I.e. it doesn't try to squeeze out the maximum
       
   141 # performance, but is simpler has a better chance of actually
       
   142 # working correctly.  And this gives the biggest performance win: it
       
   143 # avoids looking up .hgtags in the manifest for every head, and it
       
   144 # can avoid calling heads() at all if there have been no changes to
       
   145 # the repo.
       
   146 
       
   147 def _readtagcache(ui, repo):
       
   148     '''Read the tag cache and return a tuple (heads, fnodes, cachetags,
       
   149     shouldwrite).  If the cache is completely up-to-date, cachetags is a
       
   150     dict of the form returned by _readtags(); otherwise, it is None and
       
   151     heads and fnodes are set.  In that case, heads is the list of all
       
   152     heads currently in the repository (ordered from tip to oldest) and
       
   153     fnodes is a mapping from head to .hgtags filenode.  If those two are
       
   154     set, caller is responsible for reading tag info from each head.'''
       
   155 
       
   156     try:
       
   157         cachefile = repo.opener('tags.cache', 'r')
       
   158         # force reading the file for static-http
       
   159         cachelines = iter(cachefile)
       
   160     except IOError:
       
   161         cachefile = None
       
   162 
       
   163     # The cache file consists of lines like
       
   164     #   <headrev> <headnode> [<tagnode>]
       
   165     # where <headrev> and <headnode> redundantly identify a repository
       
   166     # head from the time the cache was written, and <tagnode> is the
       
   167     # filenode of .hgtags on that head.  Heads with no .hgtags file will
       
   168     # have no <tagnode>.  The cache is ordered from tip to oldest (which
       
   169     # is part of why <headrev> is there: a quick visual check is all
       
   170     # that's required to ensure correct order).
       
   171     #
       
   172     # This information is enough to let us avoid the most expensive part
       
   173     # of finding global tags, which is looking up <tagnode> in the
       
   174     # manifest for each head.
       
   175     cacherevs = []                      # list of headrev
       
   176     cacheheads = []                     # list of headnode
       
   177     cachefnode = {}                     # map headnode to filenode
       
   178     if cachefile:
       
   179         try:
       
   180             for line in cachelines:
       
   181                 if line == "\n":
       
   182                     break
       
   183                 line = line.rstrip().split()
       
   184                 cacherevs.append(int(line[0]))
       
   185                 headnode = bin(line[1])
       
   186                 cacheheads.append(headnode)
       
   187                 if len(line) == 3:
       
   188                     fnode = bin(line[2])
       
   189                     cachefnode[headnode] = fnode
       
   190         except (ValueError, TypeError):
       
   191             # corruption of tags.cache, just recompute it
       
   192             ui.warn(_('.hg/tags.cache is corrupt, rebuilding it\n'))
       
   193             cacheheads = []
       
   194             cacherevs = []
       
   195             cachefnode = {}
       
   196 
       
   197     tipnode = repo.changelog.tip()
       
   198     tiprev = len(repo.changelog) - 1
       
   199 
       
   200     # Case 1 (common): tip is the same, so nothing has changed.
       
   201     # (Unchanged tip trivially means no changesets have been added.
       
   202     # But, thanks to localrepository.destroyed(), it also means none
       
   203     # have been destroyed by strip or rollback.)
       
   204     if cacheheads and cacheheads[0] == tipnode and cacherevs[0] == tiprev:
       
   205         tags = _readtags(ui, repo, cachelines, cachefile.name)
       
   206         cachefile.close()
       
   207         return (None, None, tags, False)
       
   208     if cachefile:
       
   209         cachefile.close()               # ignore rest of file
       
   210 
       
   211     repoheads = repo.heads()
       
   212     # Case 2 (uncommon): empty repo; get out quickly and don't bother
       
   213     # writing an empty cache.
       
   214     if repoheads == [nullid]:
       
   215         return ([], {}, {}, False)
       
   216 
       
   217     # Case 3 (uncommon): cache file missing or empty.
       
   218 
       
   219     # Case 4 (uncommon): tip rev decreased.  This should only happen
       
   220     # when we're called from localrepository.destroyed().  Refresh the
       
   221     # cache so future invocations will not see disappeared heads in the
       
   222     # cache.
       
   223 
       
   224     # Case 5 (common): tip has changed, so we've added/replaced heads.
       
   225 
       
   226     # As it happens, the code to handle cases 3, 4, 5 is the same.
       
   227 
       
   228     # N.B. in case 4 (nodes destroyed), "new head" really means "newly
       
   229     # exposed".
       
   230     newheads = [head
       
   231                 for head in repoheads
       
   232                 if head not in set(cacheheads)]
       
   233 
       
   234     # Now we have to lookup the .hgtags filenode for every new head.
       
   235     # This is the most expensive part of finding tags, so performance
       
   236     # depends primarily on the size of newheads.  Worst case: no cache
       
   237     # file, so newheads == repoheads.
       
   238     for head in newheads:
       
   239         cctx = repo[head]
       
   240         try:
       
   241             fnode = cctx.filenode('.hgtags')
       
   242             cachefnode[head] = fnode
       
   243         except error.LookupError:
       
   244             # no .hgtags file on this head
       
   245             pass
       
   246 
       
   247     # Caller has to iterate over all heads, but can use the filenodes in
       
   248     # cachefnode to get to each .hgtags revision quickly.
       
   249     return (repoheads, cachefnode, None, True)
       
   250 
       
   251 def _writetagcache(ui, repo, heads, tagfnode, cachetags):
       
   252 
       
   253     try:
       
   254         cachefile = repo.opener('tags.cache', 'w', atomictemp=True)
       
   255     except (OSError, IOError):
       
   256         return
       
   257 
       
   258     realheads = repo.heads()            # for sanity checks below
       
   259     for head in heads:
       
   260         # temporary sanity checks; these can probably be removed
       
   261         # once this code has been in crew for a few weeks
       
   262         assert head in repo.changelog.nodemap, \
       
   263                'trying to write non-existent node %s to tag cache' % short(head)
       
   264         assert head in realheads, \
       
   265                'trying to write non-head %s to tag cache' % short(head)
       
   266         assert head != nullid, \
       
   267                'trying to write nullid to tag cache'
       
   268 
       
   269         # This can't fail because of the first assert above.  When/if we
       
   270         # remove that assert, we might want to catch LookupError here
       
   271         # and downgrade it to a warning.
       
   272         rev = repo.changelog.rev(head)
       
   273 
       
   274         fnode = tagfnode.get(head)
       
   275         if fnode:
       
   276             cachefile.write('%d %s %s\n' % (rev, hex(head), hex(fnode)))
       
   277         else:
       
   278             cachefile.write('%d %s\n' % (rev, hex(head)))
       
   279 
       
   280     # Tag names in the cache are in UTF-8 -- which is the whole reason
       
   281     # we keep them in UTF-8 throughout this module.  If we converted
       
   282     # them local encoding on input, we would lose info writing them to
       
   283     # the cache.
       
   284     cachefile.write('\n')
       
   285     for (name, (node, hist)) in cachetags.iteritems():
       
   286         cachefile.write("%s %s\n" % (hex(node), name))
       
   287 
       
   288     cachefile.rename()