PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/tags.py

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