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