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