##// END OF EJS Templates
cachevfs: migrate tagscache to 'cachevfs'...
Boris Feld -
r33536:ddacd0e4 default
parent child Browse files
Show More
@@ -1,789 +1,789 b''
1 1 # tags.py - read tag info from local repository
2 2 #
3 3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2009 Greg Ward <greg@gerg.ca>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 # Currently this module only deals with reading and caching tags.
10 10 # Eventually, it could take care of updating (adding/removing/moving)
11 11 # tags too.
12 12
13 13 from __future__ import absolute_import
14 14
15 15 import errno
16 16
17 17 from .node import (
18 18 bin,
19 19 hex,
20 20 nullid,
21 21 short,
22 22 )
23 23 from .i18n import _
24 24 from . import (
25 25 encoding,
26 26 error,
27 27 match as matchmod,
28 28 scmutil,
29 29 util,
30 30 )
31 31
32 32 # Tags computation can be expensive and caches exist to make it fast in
33 33 # the common case.
34 34 #
35 35 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
36 36 # each revision in the repository. The file is effectively an array of
37 37 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
38 38 # details.
39 39 #
40 40 # The .hgtags filenode cache grows in proportion to the length of the
41 41 # changelog. The file is truncated when the # changelog is stripped.
42 42 #
43 43 # The purpose of the filenode cache is to avoid the most expensive part
44 44 # of finding global tags, which is looking up the .hgtags filenode in the
45 45 # manifest for each head. This can take dozens or over 100ms for
46 46 # repositories with very large manifests. Multiplied by dozens or even
47 47 # hundreds of heads and there is a significant performance concern.
48 48 #
49 49 # There also exist a separate cache file for each repository filter.
50 50 # These "tags-*" files store information about the history of tags.
51 51 #
52 52 # The tags cache files consists of a cache validation line followed by
53 53 # a history of tags.
54 54 #
55 55 # The cache validation line has the format:
56 56 #
57 57 # <tiprev> <tipnode> [<filteredhash>]
58 58 #
59 59 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
60 60 # node for that changeset. These redundantly identify the repository
61 61 # tip from the time the cache was written. In addition, <filteredhash>,
62 62 # if present, is a 40 character hex hash of the contents of the filtered
63 63 # revisions for this filter. If the set of filtered revs changes, the
64 64 # hash will change and invalidate the cache.
65 65 #
66 66 # The history part of the tags cache consists of lines of the form:
67 67 #
68 68 # <node> <tag>
69 69 #
70 70 # (This format is identical to that of .hgtags files.)
71 71 #
72 72 # <tag> is the tag name and <node> is the 40 character hex changeset
73 73 # the tag is associated with.
74 74 #
75 75 # Tags are written sorted by tag name.
76 76 #
77 77 # Tags associated with multiple changesets have an entry for each changeset.
78 78 # The most recent changeset (in terms of revlog ordering for the head
79 79 # setting it) for each tag is last.
80 80
81 81 def fnoderevs(ui, repo, revs):
82 82 """return the list of '.hgtags' fnodes used in a set revisions
83 83
84 84 This is returned as list of unique fnodes. We use a list instead of a set
85 85 because order matters when it comes to tags."""
86 86 unfi = repo.unfiltered()
87 87 tonode = unfi.changelog.node
88 88 nodes = [tonode(r) for r in revs]
89 89 fnodes = _getfnodes(ui, repo, nodes[::-1]) # reversed help the cache
90 90 fnodes = _filterfnodes(fnodes, nodes)
91 91 return fnodes
92 92
93 93 def _nulltonone(value):
94 94 """convert nullid to None
95 95
96 96 For tag value, nullid means "deleted". This small utility function helps
97 97 translating that to None."""
98 98 if value == nullid:
99 99 return None
100 100 return value
101 101
102 102 def difftags(ui, repo, oldfnodes, newfnodes):
103 103 """list differences between tags expressed in two set of file-nodes
104 104
105 105 The list contains entries in the form: (tagname, oldvalue, new value).
106 106 None is used to expressed missing value:
107 107 ('foo', None, 'abcd') is a new tag,
108 108 ('bar', 'ef01', None) is a deletion,
109 109 ('baz', 'abcd', 'ef01') is a tag movement.
110 110 """
111 111 if oldfnodes == newfnodes:
112 112 return []
113 113 oldtags = _tagsfromfnodes(ui, repo, oldfnodes)
114 114 newtags = _tagsfromfnodes(ui, repo, newfnodes)
115 115
116 116 # list of (tag, old, new): None means missing
117 117 entries = []
118 118 for tag, (new, __) in newtags.items():
119 119 new = _nulltonone(new)
120 120 old, __ = oldtags.pop(tag, (None, None))
121 121 old = _nulltonone(old)
122 122 if old != new:
123 123 entries.append((tag, old, new))
124 124 # handle deleted tags
125 125 for tag, (old, __) in oldtags.items():
126 126 old = _nulltonone(old)
127 127 if old is not None:
128 128 entries.append((tag, old, None))
129 129 entries.sort()
130 130 return entries
131 131
132 132 def writediff(fp, difflist):
133 133 """write tags diff information to a file.
134 134
135 135 Data are stored with a line based format:
136 136
137 137 <action> <hex-node> <tag-name>\n
138 138
139 139 Action are defined as follow:
140 140 -R tag is removed,
141 141 +A tag is added,
142 142 -M tag is moved (old value),
143 143 +M tag is moved (new value),
144 144
145 145 Example:
146 146
147 147 +A 875517b4806a848f942811a315a5bce30804ae85 t5
148 148
149 149 See documentation of difftags output for details about the input.
150 150 """
151 151 add = '+A %s %s\n'
152 152 remove = '-R %s %s\n'
153 153 updateold = '-M %s %s\n'
154 154 updatenew = '+M %s %s\n'
155 155 for tag, old, new in difflist:
156 156 # translate to hex
157 157 if old is not None:
158 158 old = hex(old)
159 159 if new is not None:
160 160 new = hex(new)
161 161 # write to file
162 162 if old is None:
163 163 fp.write(add % (new, tag))
164 164 elif new is None:
165 165 fp.write(remove % (old, tag))
166 166 else:
167 167 fp.write(updateold % (old, tag))
168 168 fp.write(updatenew % (new, tag))
169 169
170 170 def findglobaltags(ui, repo):
171 171 '''Find global tags in a repo: return a tagsmap
172 172
173 173 tagsmap: tag name to (node, hist) 2-tuples.
174 174
175 175 The tags cache is read and updated as a side-effect of calling.
176 176 '''
177 177 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
178 178 if cachetags is not None:
179 179 assert not shouldwrite
180 180 # XXX is this really 100% correct? are there oddball special
181 181 # cases where a global tag should outrank a local tag but won't,
182 182 # because cachetags does not contain rank info?
183 183 alltags = {}
184 184 _updatetags(cachetags, alltags)
185 185 return alltags
186 186
187 187 for head in reversed(heads): # oldest to newest
188 188 assert head in repo.changelog.nodemap, \
189 189 "tag cache returned bogus head %s" % short(head)
190 190 fnodes = _filterfnodes(tagfnode, reversed(heads))
191 191 alltags = _tagsfromfnodes(ui, repo, fnodes)
192 192
193 193 # and update the cache (if necessary)
194 194 if shouldwrite:
195 195 _writetagcache(ui, repo, valid, alltags)
196 196 return alltags
197 197
198 198 def _filterfnodes(tagfnode, nodes):
199 199 """return a list of unique fnodes
200 200
201 201 The order of this list matches the order of "nodes". Preserving this order
202 202 is important as reading tags in different order provides different
203 203 results."""
204 204 seen = set() # set of fnode
205 205 fnodes = []
206 206 for no in nodes: # oldest to newest
207 207 fnode = tagfnode.get(no)
208 208 if fnode and fnode not in seen:
209 209 seen.add(fnode)
210 210 fnodes.append(fnode)
211 211 return fnodes
212 212
213 213 def _tagsfromfnodes(ui, repo, fnodes):
214 214 """return a tagsmap from a list of file-node
215 215
216 216 tagsmap: tag name to (node, hist) 2-tuples.
217 217
218 218 The order of the list matters."""
219 219 alltags = {}
220 220 fctx = None
221 221 for fnode in fnodes:
222 222 if fctx is None:
223 223 fctx = repo.filectx('.hgtags', fileid=fnode)
224 224 else:
225 225 fctx = fctx.filectx(fnode)
226 226 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
227 227 _updatetags(filetags, alltags)
228 228 return alltags
229 229
230 230 def readlocaltags(ui, repo, alltags, tagtypes):
231 231 '''Read local tags in repo. Update alltags and tagtypes.'''
232 232 try:
233 233 data = repo.vfs.read("localtags")
234 234 except IOError as inst:
235 235 if inst.errno != errno.ENOENT:
236 236 raise
237 237 return
238 238
239 239 # localtags is in the local encoding; re-encode to UTF-8 on
240 240 # input for consistency with the rest of this module.
241 241 filetags = _readtags(
242 242 ui, repo, data.splitlines(), "localtags",
243 243 recode=encoding.fromlocal)
244 244
245 245 # remove tags pointing to invalid nodes
246 246 cl = repo.changelog
247 247 for t in filetags.keys():
248 248 try:
249 249 cl.rev(filetags[t][0])
250 250 except (LookupError, ValueError):
251 251 del filetags[t]
252 252
253 253 _updatetags(filetags, alltags, 'local', tagtypes)
254 254
255 255 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
256 256 '''Read tag definitions from a file (or any source of lines).
257 257
258 258 This function returns two sortdicts with similar information:
259 259
260 260 - the first dict, bintaghist, contains the tag information as expected by
261 261 the _readtags function, i.e. a mapping from tag name to (node, hist):
262 262 - node is the node id from the last line read for that name,
263 263 - hist is the list of node ids previously associated with it (in file
264 264 order). All node ids are binary, not hex.
265 265
266 266 - the second dict, hextaglines, is a mapping from tag name to a list of
267 267 [hexnode, line number] pairs, ordered from the oldest to the newest node.
268 268
269 269 When calcnodelines is False the hextaglines dict is not calculated (an
270 270 empty dict is returned). This is done to improve this function's
271 271 performance in cases where the line numbers are not needed.
272 272 '''
273 273
274 274 bintaghist = util.sortdict()
275 275 hextaglines = util.sortdict()
276 276 count = 0
277 277
278 278 def dbg(msg):
279 279 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
280 280
281 281 for nline, line in enumerate(lines):
282 282 count += 1
283 283 if not line:
284 284 continue
285 285 try:
286 286 (nodehex, name) = line.split(" ", 1)
287 287 except ValueError:
288 288 dbg("cannot parse entry")
289 289 continue
290 290 name = name.strip()
291 291 if recode:
292 292 name = recode(name)
293 293 try:
294 294 nodebin = bin(nodehex)
295 295 except TypeError:
296 296 dbg("node '%s' is not well formed" % nodehex)
297 297 continue
298 298
299 299 # update filetags
300 300 if calcnodelines:
301 301 # map tag name to a list of line numbers
302 302 if name not in hextaglines:
303 303 hextaglines[name] = []
304 304 hextaglines[name].append([nodehex, nline])
305 305 continue
306 306 # map tag name to (node, hist)
307 307 if name not in bintaghist:
308 308 bintaghist[name] = []
309 309 bintaghist[name].append(nodebin)
310 310 return bintaghist, hextaglines
311 311
312 312 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
313 313 '''Read tag definitions from a file (or any source of lines).
314 314
315 315 Returns a mapping from tag name to (node, hist).
316 316
317 317 "node" is the node id from the last line read for that name. "hist"
318 318 is the list of node ids previously associated with it (in file order).
319 319 All node ids are binary, not hex.
320 320 '''
321 321 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
322 322 calcnodelines=calcnodelines)
323 323 # util.sortdict().__setitem__ is much slower at replacing then inserting
324 324 # new entries. The difference can matter if there are thousands of tags.
325 325 # Create a new sortdict to avoid the performance penalty.
326 326 newtags = util.sortdict()
327 327 for tag, taghist in filetags.items():
328 328 newtags[tag] = (taghist[-1], taghist[:-1])
329 329 return newtags
330 330
331 331 def _updatetags(filetags, alltags, tagtype=None, tagtypes=None):
332 332 """Incorporate the tag info read from one file into dictionnaries
333 333
334 334 The first one, 'alltags', is a "tagmaps" (see 'findglobaltags' for details).
335 335
336 336 The second one, 'tagtypes', is optional and will be updated to track the
337 337 "tagtype" of entries in the tagmaps. When set, the 'tagtype' argument also
338 338 needs to be set."""
339 339 if tagtype is None:
340 340 assert tagtypes is None
341 341
342 342 for name, nodehist in filetags.iteritems():
343 343 if name not in alltags:
344 344 alltags[name] = nodehist
345 345 if tagtype is not None:
346 346 tagtypes[name] = tagtype
347 347 continue
348 348
349 349 # we prefer alltags[name] if:
350 350 # it supersedes us OR
351 351 # mutual supersedes and it has a higher rank
352 352 # otherwise we win because we're tip-most
353 353 anode, ahist = nodehist
354 354 bnode, bhist = alltags[name]
355 355 if (bnode != anode and anode in bhist and
356 356 (bnode not in ahist or len(bhist) > len(ahist))):
357 357 anode = bnode
358 358 elif tagtype is not None:
359 359 tagtypes[name] = tagtype
360 360 ahist.extend([n for n in bhist if n not in ahist])
361 361 alltags[name] = anode, ahist
362 362
363 363 def _filename(repo):
364 364 """name of a tagcache file for a given repo or repoview"""
365 filename = 'cache/tags2'
365 filename = 'tags2'
366 366 if repo.filtername:
367 367 filename = '%s-%s' % (filename, repo.filtername)
368 368 return filename
369 369
370 370 def _readtagcache(ui, repo):
371 371 '''Read the tag cache.
372 372
373 373 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
374 374
375 375 If the cache is completely up-to-date, "cachetags" is a dict of the
376 376 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
377 377 None and "shouldwrite" is False.
378 378
379 379 If the cache is not up to date, "cachetags" is None. "heads" is a list
380 380 of all heads currently in the repository, ordered from tip to oldest.
381 381 "validinfo" is a tuple describing cache validation info. This is used
382 382 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
383 383 filenode. "shouldwrite" is True.
384 384
385 385 If the cache is not up to date, the caller is responsible for reading tag
386 386 info from each returned head. (See findglobaltags().)
387 387 '''
388 388 try:
389 cachefile = repo.vfs(_filename(repo), 'r')
389 cachefile = repo.cachevfs(_filename(repo), 'r')
390 390 # force reading the file for static-http
391 391 cachelines = iter(cachefile)
392 392 except IOError:
393 393 cachefile = None
394 394
395 395 cacherev = None
396 396 cachenode = None
397 397 cachehash = None
398 398 if cachefile:
399 399 try:
400 400 validline = next(cachelines)
401 401 validline = validline.split()
402 402 cacherev = int(validline[0])
403 403 cachenode = bin(validline[1])
404 404 if len(validline) > 2:
405 405 cachehash = bin(validline[2])
406 406 except Exception:
407 407 # corruption of the cache, just recompute it.
408 408 pass
409 409
410 410 tipnode = repo.changelog.tip()
411 411 tiprev = len(repo.changelog) - 1
412 412
413 413 # Case 1 (common): tip is the same, so nothing has changed.
414 414 # (Unchanged tip trivially means no changesets have been added.
415 415 # But, thanks to localrepository.destroyed(), it also means none
416 416 # have been destroyed by strip or rollback.)
417 417 if (cacherev == tiprev
418 418 and cachenode == tipnode
419 419 and cachehash == scmutil.filteredhash(repo, tiprev)):
420 420 tags = _readtags(ui, repo, cachelines, cachefile.name)
421 421 cachefile.close()
422 422 return (None, None, None, tags, False)
423 423 if cachefile:
424 424 cachefile.close() # ignore rest of file
425 425
426 426 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
427 427
428 428 repoheads = repo.heads()
429 429 # Case 2 (uncommon): empty repo; get out quickly and don't bother
430 430 # writing an empty cache.
431 431 if repoheads == [nullid]:
432 432 return ([], {}, valid, {}, False)
433 433
434 434 # Case 3 (uncommon): cache file missing or empty.
435 435
436 436 # Case 4 (uncommon): tip rev decreased. This should only happen
437 437 # when we're called from localrepository.destroyed(). Refresh the
438 438 # cache so future invocations will not see disappeared heads in the
439 439 # cache.
440 440
441 441 # Case 5 (common): tip has changed, so we've added/replaced heads.
442 442
443 443 # As it happens, the code to handle cases 3, 4, 5 is the same.
444 444
445 445 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
446 446 # exposed".
447 447 if not len(repo.file('.hgtags')):
448 448 # No tags have ever been committed, so we can avoid a
449 449 # potentially expensive search.
450 450 return ([], {}, valid, None, True)
451 451
452 452
453 453 # Now we have to lookup the .hgtags filenode for every new head.
454 454 # This is the most expensive part of finding tags, so performance
455 455 # depends primarily on the size of newheads. Worst case: no cache
456 456 # file, so newheads == repoheads.
457 457 cachefnode = _getfnodes(ui, repo, repoheads)
458 458
459 459 # Caller has to iterate over all heads, but can use the filenodes in
460 460 # cachefnode to get to each .hgtags revision quickly.
461 461 return (repoheads, cachefnode, valid, None, True)
462 462
463 463 def _getfnodes(ui, repo, nodes):
464 464 """return .hgtags fnodes for a list of changeset nodes
465 465
466 466 Return value is a {node: fnode} mapping. There will be no entry for nodes
467 467 without a '.hgtags' file.
468 468 """
469 469 starttime = util.timer()
470 470 fnodescache = hgtagsfnodescache(repo.unfiltered())
471 471 cachefnode = {}
472 472 for node in reversed(nodes):
473 473 fnode = fnodescache.getfnode(node)
474 474 if fnode != nullid:
475 475 cachefnode[node] = fnode
476 476
477 477 fnodescache.write()
478 478
479 479 duration = util.timer() - starttime
480 480 ui.log('tagscache',
481 481 '%d/%d cache hits/lookups in %0.4f '
482 482 'seconds\n',
483 483 fnodescache.hitcount, fnodescache.lookupcount, duration)
484 484 return cachefnode
485 485
486 486 def _writetagcache(ui, repo, valid, cachetags):
487 487 filename = _filename(repo)
488 488 try:
489 cachefile = repo.vfs(filename, 'w', atomictemp=True)
489 cachefile = repo.cachevfs(filename, 'w', atomictemp=True)
490 490 except (OSError, IOError):
491 491 return
492 492
493 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
493 ui.log('tagscache', 'writing .hg/cache/%s with %d tags\n',
494 494 filename, len(cachetags))
495 495
496 496 if valid[2]:
497 497 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
498 498 else:
499 499 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
500 500
501 501 # Tag names in the cache are in UTF-8 -- which is the whole reason
502 502 # we keep them in UTF-8 throughout this module. If we converted
503 503 # them local encoding on input, we would lose info writing them to
504 504 # the cache.
505 505 for (name, (node, hist)) in sorted(cachetags.iteritems()):
506 506 for n in hist:
507 507 cachefile.write("%s %s\n" % (hex(n), name))
508 508 cachefile.write("%s %s\n" % (hex(node), name))
509 509
510 510 try:
511 511 cachefile.close()
512 512 except (OSError, IOError):
513 513 pass
514 514
515 515 def tag(repo, names, node, message, local, user, date, editor=False):
516 516 '''tag a revision with one or more symbolic names.
517 517
518 518 names is a list of strings or, when adding a single tag, names may be a
519 519 string.
520 520
521 521 if local is True, the tags are stored in a per-repository file.
522 522 otherwise, they are stored in the .hgtags file, and a new
523 523 changeset is committed with the change.
524 524
525 525 keyword arguments:
526 526
527 527 local: whether to store tags in non-version-controlled file
528 528 (default False)
529 529
530 530 message: commit message to use if committing
531 531
532 532 user: name of user to use if committing
533 533
534 534 date: date tuple to use if committing'''
535 535
536 536 if not local:
537 537 m = matchmod.exact(repo.root, '', ['.hgtags'])
538 538 if any(repo.status(match=m, unknown=True, ignored=True)):
539 539 raise error.Abort(_('working copy of .hgtags is changed'),
540 540 hint=_('please commit .hgtags manually'))
541 541
542 542 with repo.wlock():
543 543 repo.tags() # instantiate the cache
544 544 _tag(repo.unfiltered(), names, node, message, local, user, date,
545 545 editor=editor)
546 546
547 547 def _tag(repo, names, node, message, local, user, date, extra=None,
548 548 editor=False):
549 549 if isinstance(names, str):
550 550 names = (names,)
551 551
552 552 branches = repo.branchmap()
553 553 for name in names:
554 554 repo.hook('pretag', throw=True, node=hex(node), tag=name,
555 555 local=local)
556 556 if name in branches:
557 557 repo.ui.warn(_("warning: tag %s conflicts with existing"
558 558 " branch name\n") % name)
559 559
560 560 def writetags(fp, names, munge, prevtags):
561 561 fp.seek(0, 2)
562 562 if prevtags and prevtags[-1] != '\n':
563 563 fp.write('\n')
564 564 for name in names:
565 565 if munge:
566 566 m = munge(name)
567 567 else:
568 568 m = name
569 569
570 570 if (repo._tagscache.tagtypes and
571 571 name in repo._tagscache.tagtypes):
572 572 old = repo.tags().get(name, nullid)
573 573 fp.write('%s %s\n' % (hex(old), m))
574 574 fp.write('%s %s\n' % (hex(node), m))
575 575 fp.close()
576 576
577 577 prevtags = ''
578 578 if local:
579 579 try:
580 580 fp = repo.vfs('localtags', 'r+')
581 581 except IOError:
582 582 fp = repo.vfs('localtags', 'a')
583 583 else:
584 584 prevtags = fp.read()
585 585
586 586 # local tags are stored in the current charset
587 587 writetags(fp, names, None, prevtags)
588 588 for name in names:
589 589 repo.hook('tag', node=hex(node), tag=name, local=local)
590 590 return
591 591
592 592 try:
593 593 fp = repo.wvfs('.hgtags', 'rb+')
594 594 except IOError as e:
595 595 if e.errno != errno.ENOENT:
596 596 raise
597 597 fp = repo.wvfs('.hgtags', 'ab')
598 598 else:
599 599 prevtags = fp.read()
600 600
601 601 # committed tags are stored in UTF-8
602 602 writetags(fp, names, encoding.fromlocal, prevtags)
603 603
604 604 fp.close()
605 605
606 606 repo.invalidatecaches()
607 607
608 608 if '.hgtags' not in repo.dirstate:
609 609 repo[None].add(['.hgtags'])
610 610
611 611 m = matchmod.exact(repo.root, '', ['.hgtags'])
612 612 tagnode = repo.commit(message, user, date, extra=extra, match=m,
613 613 editor=editor)
614 614
615 615 for name in names:
616 616 repo.hook('tag', node=hex(node), tag=name, local=local)
617 617
618 618 return tagnode
619 619
620 620 _fnodescachefile = 'cache/hgtagsfnodes1'
621 621 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
622 622 _fnodesmissingrec = '\xff' * 24
623 623
624 624 class hgtagsfnodescache(object):
625 625 """Persistent cache mapping revisions to .hgtags filenodes.
626 626
627 627 The cache is an array of records. Each item in the array corresponds to
628 628 a changelog revision. Values in the array contain the first 4 bytes of
629 629 the node hash and the 20 bytes .hgtags filenode for that revision.
630 630
631 631 The first 4 bytes are present as a form of verification. Repository
632 632 stripping and rewriting may change the node at a numeric revision in the
633 633 changelog. The changeset fragment serves as a verifier to detect
634 634 rewriting. This logic is shared with the rev branch cache (see
635 635 branchmap.py).
636 636
637 637 The instance holds in memory the full cache content but entries are
638 638 only parsed on read.
639 639
640 640 Instances behave like lists. ``c[i]`` works where i is a rev or
641 641 changeset node. Missing indexes are populated automatically on access.
642 642 """
643 643 def __init__(self, repo):
644 644 assert repo.filtername is None
645 645
646 646 self._repo = repo
647 647
648 648 # Only for reporting purposes.
649 649 self.lookupcount = 0
650 650 self.hitcount = 0
651 651
652 652
653 653 try:
654 654 data = repo.vfs.read(_fnodescachefile)
655 655 except (OSError, IOError):
656 656 data = ""
657 657 self._raw = bytearray(data)
658 658
659 659 # The end state of self._raw is an array that is of the exact length
660 660 # required to hold a record for every revision in the repository.
661 661 # We truncate or extend the array as necessary. self._dirtyoffset is
662 662 # defined to be the start offset at which we need to write the output
663 663 # file. This offset is also adjusted when new entries are calculated
664 664 # for array members.
665 665 cllen = len(repo.changelog)
666 666 wantedlen = cllen * _fnodesrecsize
667 667 rawlen = len(self._raw)
668 668
669 669 self._dirtyoffset = None
670 670
671 671 if rawlen < wantedlen:
672 672 self._dirtyoffset = rawlen
673 673 self._raw.extend('\xff' * (wantedlen - rawlen))
674 674 elif rawlen > wantedlen:
675 675 # There's no easy way to truncate array instances. This seems
676 676 # slightly less evil than copying a potentially large array slice.
677 677 for i in range(rawlen - wantedlen):
678 678 self._raw.pop()
679 679 self._dirtyoffset = len(self._raw)
680 680
681 681 def getfnode(self, node, computemissing=True):
682 682 """Obtain the filenode of the .hgtags file at a specified revision.
683 683
684 684 If the value is in the cache, the entry will be validated and returned.
685 685 Otherwise, the filenode will be computed and returned unless
686 686 "computemissing" is False, in which case None will be returned without
687 687 any potentially expensive computation being performed.
688 688
689 689 If an .hgtags does not exist at the specified revision, nullid is
690 690 returned.
691 691 """
692 692 ctx = self._repo[node]
693 693 rev = ctx.rev()
694 694
695 695 self.lookupcount += 1
696 696
697 697 offset = rev * _fnodesrecsize
698 698 record = '%s' % self._raw[offset:offset + _fnodesrecsize]
699 699 properprefix = node[0:4]
700 700
701 701 # Validate and return existing entry.
702 702 if record != _fnodesmissingrec:
703 703 fileprefix = record[0:4]
704 704
705 705 if fileprefix == properprefix:
706 706 self.hitcount += 1
707 707 return record[4:]
708 708
709 709 # Fall through.
710 710
711 711 # If we get here, the entry is either missing or invalid.
712 712
713 713 if not computemissing:
714 714 return None
715 715
716 716 # Populate missing entry.
717 717 try:
718 718 fnode = ctx.filenode('.hgtags')
719 719 except error.LookupError:
720 720 # No .hgtags file on this revision.
721 721 fnode = nullid
722 722
723 723 self._writeentry(offset, properprefix, fnode)
724 724 return fnode
725 725
726 726 def setfnode(self, node, fnode):
727 727 """Set the .hgtags filenode for a given changeset."""
728 728 assert len(fnode) == 20
729 729 ctx = self._repo[node]
730 730
731 731 # Do a lookup first to avoid writing if nothing has changed.
732 732 if self.getfnode(ctx.node(), computemissing=False) == fnode:
733 733 return
734 734
735 735 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
736 736
737 737 def _writeentry(self, offset, prefix, fnode):
738 738 # Slices on array instances only accept other array.
739 739 entry = bytearray(prefix + fnode)
740 740 self._raw[offset:offset + _fnodesrecsize] = entry
741 741 # self._dirtyoffset could be None.
742 742 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
743 743
744 744 def write(self):
745 745 """Perform all necessary writes to cache file.
746 746
747 747 This may no-op if no writes are needed or if a write lock could
748 748 not be obtained.
749 749 """
750 750 if self._dirtyoffset is None:
751 751 return
752 752
753 753 data = self._raw[self._dirtyoffset:]
754 754 if not data:
755 755 return
756 756
757 757 repo = self._repo
758 758
759 759 try:
760 760 lock = repo.wlock(wait=False)
761 761 except error.LockError:
762 762 repo.ui.log('tagscache',
763 763 'not writing .hg/%s because lock cannot be acquired\n' %
764 764 (_fnodescachefile))
765 765 return
766 766
767 767 try:
768 768 f = repo.vfs.open(_fnodescachefile, 'ab')
769 769 try:
770 770 # if the file has been truncated
771 771 actualoffset = f.tell()
772 772 if actualoffset < self._dirtyoffset:
773 773 self._dirtyoffset = actualoffset
774 774 data = self._raw[self._dirtyoffset:]
775 775 f.seek(self._dirtyoffset)
776 776 f.truncate()
777 777 repo.ui.log('tagscache',
778 778 'writing %d bytes to %s\n' % (
779 779 len(data), _fnodescachefile))
780 780 f.write(data)
781 781 self._dirtyoffset = None
782 782 finally:
783 783 f.close()
784 784 except (IOError, OSError) as inst:
785 785 repo.ui.log('tagscache',
786 786 "couldn't write %s: %s\n" % (
787 787 _fnodescachefile, inst))
788 788 finally:
789 789 lock.release()
General Comments 0
You need to be logged in to leave comments. Login now