##// END OF EJS Templates
tags: take lock instead of wlock before writing hgtagsfnodes1 cache...
Pulkit Goyal -
r46005:64de86fd default
parent child Browse files
Show More
@@ -1,876 +1,876 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 self._raw.extend(b'\xff' * (wantedlen - rawlen))
737 737
738 738 def getfnode(self, node, computemissing=True):
739 739 """Obtain the filenode of the .hgtags file at a specified revision.
740 740
741 741 If the value is in the cache, the entry will be validated and returned.
742 742 Otherwise, the filenode will be computed and returned unless
743 743 "computemissing" is False, in which case None will be returned without
744 744 any potentially expensive computation being performed.
745 745
746 746 If an .hgtags does not exist at the specified revision, nullid is
747 747 returned.
748 748 """
749 749 if node == nullid:
750 750 return nullid
751 751
752 752 ctx = self._repo[node]
753 753 rev = ctx.rev()
754 754
755 755 self.lookupcount += 1
756 756
757 757 offset = rev * _fnodesrecsize
758 758 record = b'%s' % self._raw[offset : offset + _fnodesrecsize]
759 759 properprefix = node[0:4]
760 760
761 761 # Validate and return existing entry.
762 762 if record != _fnodesmissingrec:
763 763 fileprefix = record[0:4]
764 764
765 765 if fileprefix == properprefix:
766 766 self.hitcount += 1
767 767 return record[4:]
768 768
769 769 # Fall through.
770 770
771 771 # If we get here, the entry is either missing or invalid.
772 772
773 773 if not computemissing:
774 774 return None
775 775
776 776 fnode = None
777 777 cl = self._repo.changelog
778 778 p1rev, p2rev = cl._uncheckedparentrevs(rev)
779 779 p1node = cl.node(p1rev)
780 780 p1fnode = self.getfnode(p1node, computemissing=False)
781 781 if p2rev != nullrev:
782 782 # There is some no-merge changeset where p1 is null and p2 is set
783 783 # Processing them as merge is just slower, but still gives a good
784 784 # result.
785 785 p2node = cl.node(p1rev)
786 786 p2fnode = self.getfnode(p2node, computemissing=False)
787 787 if p1fnode != p2fnode:
788 788 # we cannot rely on readfast because we don't know against what
789 789 # parent the readfast delta is computed
790 790 p1fnode = None
791 791 if p1fnode is not None:
792 792 mctx = ctx.manifestctx()
793 793 fnode = mctx.readfast().get(b'.hgtags')
794 794 if fnode is None:
795 795 fnode = p1fnode
796 796 if fnode is None:
797 797 # Populate missing entry.
798 798 try:
799 799 fnode = ctx.filenode(b'.hgtags')
800 800 except error.LookupError:
801 801 # No .hgtags file on this revision.
802 802 fnode = nullid
803 803
804 804 self._writeentry(offset, properprefix, fnode)
805 805 return fnode
806 806
807 807 def setfnode(self, node, fnode):
808 808 """Set the .hgtags filenode for a given changeset."""
809 809 assert len(fnode) == 20
810 810 ctx = self._repo[node]
811 811
812 812 # Do a lookup first to avoid writing if nothing has changed.
813 813 if self.getfnode(ctx.node(), computemissing=False) == fnode:
814 814 return
815 815
816 816 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
817 817
818 818 def _writeentry(self, offset, prefix, fnode):
819 819 # Slices on array instances only accept other array.
820 820 entry = bytearray(prefix + fnode)
821 821 self._raw[offset : offset + _fnodesrecsize] = entry
822 822 # self._dirtyoffset could be None.
823 823 self._dirtyoffset = min(self._dirtyoffset or 0, offset or 0)
824 824
825 825 def write(self):
826 826 """Perform all necessary writes to cache file.
827 827
828 828 This may no-op if no writes are needed or if a write lock could
829 829 not be obtained.
830 830 """
831 831 if self._dirtyoffset is None:
832 832 return
833 833
834 834 data = self._raw[self._dirtyoffset :]
835 835 if not data:
836 836 return
837 837
838 838 repo = self._repo
839 839
840 840 try:
841 lock = repo.wlock(wait=False)
841 lock = repo.lock(wait=False)
842 842 except error.LockError:
843 843 repo.ui.log(
844 844 b'tagscache',
845 845 b'not writing .hg/cache/%s because '
846 846 b'lock cannot be acquired\n' % _fnodescachefile,
847 847 )
848 848 return
849 849
850 850 try:
851 851 f = repo.cachevfs.open(_fnodescachefile, b'ab')
852 852 try:
853 853 # if the file has been truncated
854 854 actualoffset = f.tell()
855 855 if actualoffset < self._dirtyoffset:
856 856 self._dirtyoffset = actualoffset
857 857 data = self._raw[self._dirtyoffset :]
858 858 f.seek(self._dirtyoffset)
859 859 f.truncate()
860 860 repo.ui.log(
861 861 b'tagscache',
862 862 b'writing %d bytes to cache/%s\n'
863 863 % (len(data), _fnodescachefile),
864 864 )
865 865 f.write(data)
866 866 self._dirtyoffset = None
867 867 finally:
868 868 f.close()
869 869 except (IOError, OSError) as inst:
870 870 repo.ui.log(
871 871 b'tagscache',
872 872 b"couldn't write cache/%s: %s\n"
873 873 % (_fnodescachefile, stringutil.forcebytestr(inst)),
874 874 )
875 875 finally:
876 876 lock.release()
@@ -1,848 +1,848 b''
1 1 setup
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > blackbox=
6 6 > mock=$TESTDIR/mockblackbox.py
7 7 > [blackbox]
8 8 > track = command, commandfinish, tagscache
9 9 > EOF
10 10
11 11 Helper functions:
12 12
13 13 $ cacheexists() {
14 14 > [ -f .hg/cache/tags2-visible ] && echo "tag cache exists" || echo "no tag cache"
15 15 > }
16 16
17 17 $ fnodescacheexists() {
18 18 > [ -f .hg/cache/hgtagsfnodes1 ] && echo "fnodes cache exists" || echo "no fnodes cache"
19 19 > }
20 20
21 21 $ dumptags() {
22 22 > rev=$1
23 23 > echo "rev $rev: .hgtags:"
24 24 > hg cat -r$rev .hgtags
25 25 > }
26 26
27 27 # XXX need to test that the tag cache works when we strip an old head
28 28 # and add a new one rooted off non-tip: i.e. node and rev of tip are the
29 29 # same, but stuff has changed behind tip.
30 30
31 31 Setup:
32 32
33 33 $ hg init t
34 34 $ cd t
35 35 $ cacheexists
36 36 no tag cache
37 37 $ fnodescacheexists
38 38 no fnodes cache
39 39 $ hg id
40 40 000000000000 tip
41 41 $ cacheexists
42 42 no tag cache
43 43 $ fnodescacheexists
44 44 no fnodes cache
45 45 $ echo a > a
46 46 $ hg add a
47 47 $ hg commit -m "test"
48 48 $ hg co
49 49 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
50 50 $ hg identify
51 51 acb14030fe0a tip
52 52 $ hg identify -r 'wdir()'
53 53 acb14030fe0a tip
54 54 $ cacheexists
55 55 tag cache exists
56 56 No fnodes cache because .hgtags file doesn't exist
57 57 (this is an implementation detail)
58 58 $ fnodescacheexists
59 59 no fnodes cache
60 60
61 61 Try corrupting the cache
62 62
63 63 $ printf 'a b' > .hg/cache/tags2-visible
64 64 $ hg identify
65 65 acb14030fe0a tip
66 66 $ cacheexists
67 67 tag cache exists
68 68 $ fnodescacheexists
69 69 no fnodes cache
70 70 $ hg identify
71 71 acb14030fe0a tip
72 72
73 73 Create local tag with long name:
74 74
75 75 $ T=`hg identify --debug --id`
76 76 $ hg tag -l "This is a local tag with a really long name!"
77 77 $ hg tags
78 78 tip 0:acb14030fe0a
79 79 This is a local tag with a really long name! 0:acb14030fe0a
80 80 $ rm .hg/localtags
81 81
82 82 Create a tag behind hg's back:
83 83
84 84 $ echo "$T first" > .hgtags
85 85 $ cat .hgtags
86 86 acb14030fe0a21b60322c440ad2d20cf7685a376 first
87 87 $ hg add .hgtags
88 88 $ hg commit -m "add tags"
89 89 $ hg tags
90 90 tip 1:b9154636be93
91 91 first 0:acb14030fe0a
92 92 $ hg identify
93 93 b9154636be93 tip
94 94
95 95 We should have a fnodes cache now that we have a real tag
96 96 The cache should have an empty entry for rev 0 and a valid entry for rev 1.
97 97
98 98
99 99 $ fnodescacheexists
100 100 fnodes cache exists
101 101 $ f --size --hexdump .hg/cache/hgtagsfnodes1
102 102 .hg/cache/hgtagsfnodes1: size=48
103 103 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
104 104 0010: ff ff ff ff ff ff ff ff b9 15 46 36 26 b7 b4 a7 |..........F6&...|
105 105 0020: 73 e0 9e e3 c5 2f 51 0e 19 e0 5e 1f f9 66 d8 59 |s..../Q...^..f.Y|
106 106 $ hg debugtagscache
107 107 0 acb14030fe0a21b60322c440ad2d20cf7685a376 missing/invalid
108 108 1 b9154636be938d3d431e75a7c906504a079bfe07 26b7b4a773e09ee3c52f510e19e05e1ff966d859
109 109
110 110 Repeat with cold tag cache:
111 111
112 112 $ rm -f .hg/cache/tags2-visible .hg/cache/hgtagsfnodes1
113 113 $ hg identify
114 114 b9154636be93 tip
115 115
116 116 $ fnodescacheexists
117 117 fnodes cache exists
118 118 $ f --size --hexdump .hg/cache/hgtagsfnodes1
119 119 .hg/cache/hgtagsfnodes1: size=48
120 120 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
121 121 0010: ff ff ff ff ff ff ff ff b9 15 46 36 26 b7 b4 a7 |..........F6&...|
122 122 0020: 73 e0 9e e3 c5 2f 51 0e 19 e0 5e 1f f9 66 d8 59 |s..../Q...^..f.Y|
123 123
124 124 And again, but now unable to write tag cache or lock file:
125 125
126 126 #if unix-permissions no-fsmonitor
127 127
128 128 $ rm -f .hg/cache/tags2-visible .hg/cache/hgtagsfnodes1
129 129 $ chmod 555 .hg/cache
130 130 $ hg identify
131 131 b9154636be93 tip
132 132 $ chmod 755 .hg/cache
133 133
134 134 (this block should be protected by no-fsmonitor, because "chmod 555 .hg"
135 135 makes watchman fail at accessing to files under .hg)
136 136
137 137 $ chmod 555 .hg
138 138 $ hg identify
139 139 b9154636be93 tip
140 140 $ chmod 755 .hg
141 141 #endif
142 142
143 143 Tag cache debug info written to blackbox log
144 144
145 145 $ rm -f .hg/cache/tags2-visible .hg/cache/hgtagsfnodes1
146 146 $ hg identify
147 147 b9154636be93 tip
148 148 $ hg blackbox -l 6
149 149 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> identify
150 150 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> writing 48 bytes to cache/hgtagsfnodes1
151 151 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> 0/2 cache hits/lookups in * seconds (glob)
152 152 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> writing .hg/cache/tags2-visible with 1 tags
153 153 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> identify exited 0 after * seconds (glob)
154 154 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> blackbox -l 6
155 155
156 156 Failure to acquire lock results in no write
157 157
158 158 $ rm -f .hg/cache/tags2-visible .hg/cache/hgtagsfnodes1
159 $ echo 'foo:1' > .hg/wlock
159 $ echo 'foo:1' > .hg/store/lock
160 160 $ hg identify
161 161 b9154636be93 tip
162 162 $ hg blackbox -l 6
163 163 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> identify
164 164 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> not writing .hg/cache/hgtagsfnodes1 because lock cannot be acquired
165 165 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> 0/2 cache hits/lookups in * seconds (glob)
166 166 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> writing .hg/cache/tags2-visible with 1 tags
167 167 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> identify exited 0 after * seconds (glob)
168 168 1970/01/01 00:00:00 bob @b9154636be938d3d431e75a7c906504a079bfe07 (5000)> blackbox -l 6
169 169
170 170 $ fnodescacheexists
171 171 no fnodes cache
172 172
173 $ rm .hg/wlock
173 $ rm .hg/store/lock
174 174
175 175 $ rm -f .hg/cache/tags2-visible .hg/cache/hgtagsfnodes1
176 176 $ hg identify
177 177 b9154636be93 tip
178 178
179 179 Create a branch:
180 180
181 181 $ echo bb > a
182 182 $ hg status
183 183 M a
184 184 $ hg identify
185 185 b9154636be93+ tip
186 186 $ hg co first
187 187 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
188 188 $ hg id
189 189 acb14030fe0a+ first
190 190 $ hg id -r 'wdir()'
191 191 acb14030fe0a+ first
192 192 $ hg -v id
193 193 acb14030fe0a+ first
194 194 $ hg status
195 195 M a
196 196 $ echo 1 > b
197 197 $ hg add b
198 198 $ hg commit -m "branch"
199 199 created new head
200 200
201 201 Creating a new commit shouldn't append the .hgtags fnodes cache until
202 202 tags info is accessed
203 203
204 204 $ f --size --hexdump .hg/cache/hgtagsfnodes1
205 205 .hg/cache/hgtagsfnodes1: size=48
206 206 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
207 207 0010: ff ff ff ff ff ff ff ff b9 15 46 36 26 b7 b4 a7 |..........F6&...|
208 208 0020: 73 e0 9e e3 c5 2f 51 0e 19 e0 5e 1f f9 66 d8 59 |s..../Q...^..f.Y|
209 209
210 210 $ hg id
211 211 c8edf04160c7 tip
212 212
213 213 First 4 bytes of record 3 are changeset fragment
214 214
215 215 $ f --size --hexdump .hg/cache/hgtagsfnodes1
216 216 .hg/cache/hgtagsfnodes1: size=72
217 217 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
218 218 0010: ff ff ff ff ff ff ff ff b9 15 46 36 26 b7 b4 a7 |..........F6&...|
219 219 0020: 73 e0 9e e3 c5 2f 51 0e 19 e0 5e 1f f9 66 d8 59 |s..../Q...^..f.Y|
220 220 0030: c8 ed f0 41 00 00 00 00 00 00 00 00 00 00 00 00 |...A............|
221 221 0040: 00 00 00 00 00 00 00 00 |........|
222 222
223 223 Merge the two heads:
224 224
225 225 $ hg merge 1
226 226 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
227 227 (branch merge, don't forget to commit)
228 228 $ hg blackbox -l3
229 229 1970/01/01 00:00:00 bob @c8edf04160c7f731e4589d66ab3ab3486a64ac28 (5000)> merge 1
230 230 1970/01/01 00:00:00 bob @c8edf04160c7f731e4589d66ab3ab3486a64ac28+b9154636be938d3d431e75a7c906504a079bfe07 (5000)> merge 1 exited 0 after * seconds (glob)
231 231 1970/01/01 00:00:00 bob @c8edf04160c7f731e4589d66ab3ab3486a64ac28+b9154636be938d3d431e75a7c906504a079bfe07 (5000)> blackbox -l3
232 232 $ hg id
233 233 c8edf04160c7+b9154636be93+ tip
234 234 $ hg status
235 235 M .hgtags
236 236 $ hg commit -m "merge"
237 237
238 238 Create a fake head, make sure tag not visible afterwards:
239 239
240 240 $ cp .hgtags tags
241 241 $ hg tag last
242 242 $ hg rm .hgtags
243 243 $ hg commit -m "remove"
244 244
245 245 $ mv tags .hgtags
246 246 $ hg add .hgtags
247 247 $ hg commit -m "readd"
248 248 $
249 249 $ hg tags
250 250 tip 6:35ff301afafe
251 251 first 0:acb14030fe0a
252 252
253 253 Add invalid tags:
254 254
255 255 $ echo "spam" >> .hgtags
256 256 $ echo >> .hgtags
257 257 $ echo "foo bar" >> .hgtags
258 258 $ echo "a5a5 invalid" >> .hg/localtags
259 259 $ cat .hgtags
260 260 acb14030fe0a21b60322c440ad2d20cf7685a376 first
261 261 spam
262 262
263 263 foo bar
264 264 $ hg commit -m "tags"
265 265
266 266 Report tag parse error on other head:
267 267
268 268 $ hg up 3
269 269 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
270 270 $ echo 'x y' >> .hgtags
271 271 $ hg commit -m "head"
272 272 created new head
273 273
274 274 $ hg tags --debug
275 275 .hgtags@75d9f02dfe28, line 2: cannot parse entry
276 276 .hgtags@75d9f02dfe28, line 4: node 'foo' is not well formed
277 277 .hgtags@c4be69a18c11, line 2: node 'x' is not well formed
278 278 tip 8:c4be69a18c11e8bc3a5fdbb576017c25f7d84663
279 279 first 0:acb14030fe0a21b60322c440ad2d20cf7685a376
280 280 $ hg tip
281 281 changeset: 8:c4be69a18c11
282 282 tag: tip
283 283 parent: 3:ac5e980c4dc0
284 284 user: test
285 285 date: Thu Jan 01 00:00:00 1970 +0000
286 286 summary: head
287 287
288 288
289 289 Test tag precedence rules:
290 290
291 291 $ cd ..
292 292 $ hg init t2
293 293 $ cd t2
294 294 $ echo foo > foo
295 295 $ hg add foo
296 296 $ hg ci -m 'add foo' # rev 0
297 297 $ hg tag bar # rev 1
298 298 $ echo >> foo
299 299 $ hg ci -m 'change foo 1' # rev 2
300 300 $ hg up -C 1
301 301 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
302 302 $ hg tag -r 1 -f bar # rev 3
303 303 $ hg up -C 1
304 304 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
305 305 $ echo >> foo
306 306 $ hg ci -m 'change foo 2' # rev 4
307 307 created new head
308 308 $ hg tags
309 309 tip 4:0c192d7d5e6b
310 310 bar 1:78391a272241
311 311
312 312 Repeat in case of cache effects:
313 313
314 314 $ hg tags
315 315 tip 4:0c192d7d5e6b
316 316 bar 1:78391a272241
317 317
318 318 Detailed dump of tag info:
319 319
320 320 $ hg heads -q # expect 4, 3, 2
321 321 4:0c192d7d5e6b
322 322 3:6fa450212aeb
323 323 2:7a94127795a3
324 324 $ dumptags 2
325 325 rev 2: .hgtags:
326 326 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
327 327 $ dumptags 3
328 328 rev 3: .hgtags:
329 329 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
330 330 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
331 331 78391a272241d70354aa14c874552cad6b51bb42 bar
332 332 $ dumptags 4
333 333 rev 4: .hgtags:
334 334 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
335 335
336 336 Dump cache:
337 337
338 338 $ cat .hg/cache/tags2-visible
339 339 4 0c192d7d5e6b78a714de54a2e9627952a877e25a
340 340 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
341 341 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
342 342 78391a272241d70354aa14c874552cad6b51bb42 bar
343 343
344 344 $ f --size --hexdump .hg/cache/hgtagsfnodes1
345 345 .hg/cache/hgtagsfnodes1: size=120
346 346 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
347 347 0010: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
348 348 0020: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
349 349 0030: 7a 94 12 77 0c 04 f2 a8 af 31 de 17 fa b7 42 28 |z..w.....1....B(|
350 350 0040: 78 ee 5a 2d ad bc 94 3d 6f a4 50 21 7d 3b 71 8c |x.Z-...=o.P!};q.|
351 351 0050: 96 4e f3 7b 89 e5 50 eb da fd 57 89 e7 6c e1 b0 |.N.{..P...W..l..|
352 352 0060: 0c 19 2d 7d 0c 04 f2 a8 af 31 de 17 fa b7 42 28 |..-}.....1....B(|
353 353 0070: 78 ee 5a 2d ad bc 94 3d |x.Z-...=|
354 354
355 355 Corrupt the .hgtags fnodes cache
356 356 Extra junk data at the end should get overwritten on next cache update
357 357
358 358 $ echo extra >> .hg/cache/hgtagsfnodes1
359 359 $ echo dummy1 > foo
360 360 $ hg commit -m throwaway1
361 361
362 362 $ hg tags
363 363 tip 5:8dbfe60eff30
364 364 bar 1:78391a272241
365 365
366 366 $ hg blackbox -l 6
367 367 1970/01/01 00:00:00 bob @8dbfe60eff306a54259cfe007db9e330e7ecf866 (5000)> tags
368 368 1970/01/01 00:00:00 bob @8dbfe60eff306a54259cfe007db9e330e7ecf866 (5000)> writing 24 bytes to cache/hgtagsfnodes1
369 369 1970/01/01 00:00:00 bob @8dbfe60eff306a54259cfe007db9e330e7ecf866 (5000)> 3/4 cache hits/lookups in * seconds (glob)
370 370 1970/01/01 00:00:00 bob @8dbfe60eff306a54259cfe007db9e330e7ecf866 (5000)> writing .hg/cache/tags2-visible with 1 tags
371 371 1970/01/01 00:00:00 bob @8dbfe60eff306a54259cfe007db9e330e7ecf866 (5000)> tags exited 0 after * seconds (glob)
372 372 1970/01/01 00:00:00 bob @8dbfe60eff306a54259cfe007db9e330e7ecf866 (5000)> blackbox -l 6
373 373
374 374 On junk data + missing cache entries, hg also overwrites the junk.
375 375
376 376 $ rm -f .hg/cache/tags2-visible
377 377 >>> import os
378 378 >>> with open(".hg/cache/hgtagsfnodes1", "ab+") as fp:
379 379 ... fp.seek(-10, os.SEEK_END) and None
380 380 ... fp.truncate() and None
381 381
382 382 $ hg debugtagscache | tail -2
383 383 4 0c192d7d5e6b78a714de54a2e9627952a877e25a 0c04f2a8af31de17fab7422878ee5a2dadbc943d
384 384 5 8dbfe60eff306a54259cfe007db9e330e7ecf866 missing/invalid
385 385 $ hg tags
386 386 tip 5:8dbfe60eff30
387 387 bar 1:78391a272241
388 388 $ hg debugtagscache | tail -2
389 389 4 0c192d7d5e6b78a714de54a2e9627952a877e25a 0c04f2a8af31de17fab7422878ee5a2dadbc943d
390 390 5 8dbfe60eff306a54259cfe007db9e330e7ecf866 0c04f2a8af31de17fab7422878ee5a2dadbc943d
391 391
392 392 #if unix-permissions no-root
393 393 Errors writing to .hgtags fnodes cache are silently ignored
394 394
395 395 $ echo dummy2 > foo
396 396 $ hg commit -m throwaway2
397 397
398 398 $ chmod a-w .hg/cache/hgtagsfnodes1
399 399 $ rm -f .hg/cache/tags2-visible
400 400
401 401 $ hg tags
402 402 tip 6:b968051b5cf3
403 403 bar 1:78391a272241
404 404
405 405 $ hg blackbox -l 6
406 406 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> tags
407 407 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> couldn't write cache/hgtagsfnodes1: [Errno *] * (glob)
408 408 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> 3/4 cache hits/lookups in * seconds (glob)
409 409 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> writing .hg/cache/tags2-visible with 1 tags
410 410 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> tags exited 0 after * seconds (glob)
411 411 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> blackbox -l 6
412 412
413 413 $ chmod a+w .hg/cache/hgtagsfnodes1
414 414
415 415 $ rm -f .hg/cache/tags2-visible
416 416 $ hg tags
417 417 tip 6:b968051b5cf3
418 418 bar 1:78391a272241
419 419
420 420 $ hg blackbox -l 6
421 421 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> tags
422 422 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> writing 24 bytes to cache/hgtagsfnodes1
423 423 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> 3/4 cache hits/lookups in * seconds (glob)
424 424 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> writing .hg/cache/tags2-visible with 1 tags
425 425 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> tags exited 0 after * seconds (glob)
426 426 1970/01/01 00:00:00 bob @b968051b5cf3f624b771779c6d5f84f1d4c3fb5d (5000)> blackbox -l 6
427 427
428 428 $ f --size .hg/cache/hgtagsfnodes1
429 429 .hg/cache/hgtagsfnodes1: size=168
430 430
431 431 $ hg -q --config extensions.strip= strip -r 6 --no-backup
432 432 #endif
433 433
434 434 Stripping doesn't truncate the tags cache until new data is available
435 435
436 436 $ rm -f .hg/cache/hgtagsfnodes1 .hg/cache/tags2-visible
437 437 $ hg tags
438 438 tip 5:8dbfe60eff30
439 439 bar 1:78391a272241
440 440
441 441 $ f --size .hg/cache/hgtagsfnodes1
442 442 .hg/cache/hgtagsfnodes1: size=144
443 443
444 444 $ hg -q --config extensions.strip= strip -r 5 --no-backup
445 445 $ hg tags
446 446 tip 4:0c192d7d5e6b
447 447 bar 1:78391a272241
448 448
449 449 $ hg blackbox -l 5
450 450 1970/01/01 00:00:00 bob @0c192d7d5e6b78a714de54a2e9627952a877e25a (5000)> writing 24 bytes to cache/hgtagsfnodes1
451 451 1970/01/01 00:00:00 bob @0c192d7d5e6b78a714de54a2e9627952a877e25a (5000)> 2/4 cache hits/lookups in * seconds (glob)
452 452 1970/01/01 00:00:00 bob @0c192d7d5e6b78a714de54a2e9627952a877e25a (5000)> writing .hg/cache/tags2-visible with 1 tags
453 453 1970/01/01 00:00:00 bob @0c192d7d5e6b78a714de54a2e9627952a877e25a (5000)> tags exited 0 after * seconds (glob)
454 454 1970/01/01 00:00:00 bob @0c192d7d5e6b78a714de54a2e9627952a877e25a (5000)> blackbox -l 5
455 455
456 456 $ f --size .hg/cache/hgtagsfnodes1
457 457 .hg/cache/hgtagsfnodes1: size=120
458 458
459 459 $ echo dummy > foo
460 460 $ hg commit -m throwaway3
461 461
462 462 $ hg tags
463 463 tip 5:035f65efb448
464 464 bar 1:78391a272241
465 465
466 466 $ hg blackbox -l 6
467 467 1970/01/01 00:00:00 bob @035f65efb448350f4772141702a81ab1df48c465 (5000)> tags
468 468 1970/01/01 00:00:00 bob @035f65efb448350f4772141702a81ab1df48c465 (5000)> writing 24 bytes to cache/hgtagsfnodes1
469 469 1970/01/01 00:00:00 bob @035f65efb448350f4772141702a81ab1df48c465 (5000)> 3/4 cache hits/lookups in * seconds (glob)
470 470 1970/01/01 00:00:00 bob @035f65efb448350f4772141702a81ab1df48c465 (5000)> writing .hg/cache/tags2-visible with 1 tags
471 471 1970/01/01 00:00:00 bob @035f65efb448350f4772141702a81ab1df48c465 (5000)> tags exited 0 after * seconds (glob)
472 472 1970/01/01 00:00:00 bob @035f65efb448350f4772141702a81ab1df48c465 (5000)> blackbox -l 6
473 473 $ f --size .hg/cache/hgtagsfnodes1
474 474 .hg/cache/hgtagsfnodes1: size=144
475 475
476 476 $ hg -q --config extensions.strip= strip -r 5 --no-backup
477 477
478 478 Test tag removal:
479 479
480 480 $ hg tag --remove bar # rev 5
481 481 $ hg tip -vp
482 482 changeset: 5:5f6e8655b1c7
483 483 tag: tip
484 484 user: test
485 485 date: Thu Jan 01 00:00:00 1970 +0000
486 486 files: .hgtags
487 487 description:
488 488 Removed tag bar
489 489
490 490
491 491 diff -r 0c192d7d5e6b -r 5f6e8655b1c7 .hgtags
492 492 --- a/.hgtags Thu Jan 01 00:00:00 1970 +0000
493 493 +++ b/.hgtags Thu Jan 01 00:00:00 1970 +0000
494 494 @@ -1,1 +1,3 @@
495 495 bbd179dfa0a71671c253b3ae0aa1513b60d199fa bar
496 496 +78391a272241d70354aa14c874552cad6b51bb42 bar
497 497 +0000000000000000000000000000000000000000 bar
498 498
499 499 $ hg tags
500 500 tip 5:5f6e8655b1c7
501 501 $ hg tags # again, try to expose cache bugs
502 502 tip 5:5f6e8655b1c7
503 503
504 504 Remove nonexistent tag:
505 505
506 506 $ hg tag --remove foobar
507 507 abort: tag 'foobar' does not exist
508 508 [255]
509 509 $ hg tip
510 510 changeset: 5:5f6e8655b1c7
511 511 tag: tip
512 512 user: test
513 513 date: Thu Jan 01 00:00:00 1970 +0000
514 514 summary: Removed tag bar
515 515
516 516
517 517 Undo a tag with rollback:
518 518
519 519 $ hg rollback # destroy rev 5 (restore bar)
520 520 repository tip rolled back to revision 4 (undo commit)
521 521 working directory now based on revision 4
522 522 $ hg tags
523 523 tip 4:0c192d7d5e6b
524 524 bar 1:78391a272241
525 525 $ hg tags
526 526 tip 4:0c192d7d5e6b
527 527 bar 1:78391a272241
528 528
529 529 Test tag rank:
530 530
531 531 $ cd ..
532 532 $ hg init t3
533 533 $ cd t3
534 534 $ echo foo > foo
535 535 $ hg add foo
536 536 $ hg ci -m 'add foo' # rev 0
537 537 $ hg tag -f bar # rev 1 bar -> 0
538 538 $ hg tag -f bar # rev 2 bar -> 1
539 539 $ hg tag -fr 0 bar # rev 3 bar -> 0
540 540 $ hg tag -fr 1 bar # rev 4 bar -> 1
541 541 $ hg tag -fr 0 bar # rev 5 bar -> 0
542 542 $ hg tags
543 543 tip 5:85f05169d91d
544 544 bar 0:bbd179dfa0a7
545 545 $ hg co 3
546 546 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
547 547 $ echo barbar > foo
548 548 $ hg ci -m 'change foo' # rev 6
549 549 created new head
550 550 $ hg tags
551 551 tip 6:735c3ca72986
552 552 bar 0:bbd179dfa0a7
553 553
554 554 Don't allow moving tag without -f:
555 555
556 556 $ hg tag -r 3 bar
557 557 abort: tag 'bar' already exists (use -f to force)
558 558 [255]
559 559 $ hg tags
560 560 tip 6:735c3ca72986
561 561 bar 0:bbd179dfa0a7
562 562
563 563 Strip 1: expose an old head:
564 564
565 565 $ hg --config extensions.mq= strip 5
566 566 saved backup bundle to $TESTTMP/t3/.hg/strip-backup/*-backup.hg (glob)
567 567 $ hg tags # partly stale cache
568 568 tip 5:735c3ca72986
569 569 bar 1:78391a272241
570 570 $ hg tags # up-to-date cache
571 571 tip 5:735c3ca72986
572 572 bar 1:78391a272241
573 573
574 574 Strip 2: destroy whole branch, no old head exposed
575 575
576 576 $ hg --config extensions.mq= strip 4
577 577 saved backup bundle to $TESTTMP/t3/.hg/strip-backup/*-backup.hg (glob)
578 578 $ hg tags # partly stale
579 579 tip 4:735c3ca72986
580 580 bar 0:bbd179dfa0a7
581 581 $ rm -f .hg/cache/tags2-visible
582 582 $ hg tags # cold cache
583 583 tip 4:735c3ca72986
584 584 bar 0:bbd179dfa0a7
585 585
586 586 Test tag rank with 3 heads:
587 587
588 588 $ cd ..
589 589 $ hg init t4
590 590 $ cd t4
591 591 $ echo foo > foo
592 592 $ hg add
593 593 adding foo
594 594 $ hg ci -m 'add foo' # rev 0
595 595 $ hg tag bar # rev 1 bar -> 0
596 596 $ hg tag -f bar # rev 2 bar -> 1
597 597 $ hg up -qC 0
598 598 $ hg tag -fr 2 bar # rev 3 bar -> 2
599 599 $ hg tags
600 600 tip 3:197c21bbbf2c
601 601 bar 2:6fa450212aeb
602 602 $ hg up -qC 0
603 603 $ hg tag -m 'retag rev 0' -fr 0 bar # rev 4 bar -> 0, but bar stays at 2
604 604
605 605 Bar should still point to rev 2:
606 606
607 607 $ hg tags
608 608 tip 4:3b4b14ed0202
609 609 bar 2:6fa450212aeb
610 610
611 611 Test that removing global/local tags does not get confused when trying
612 612 to remove a tag of type X which actually only exists as a type Y:
613 613
614 614 $ cd ..
615 615 $ hg init t5
616 616 $ cd t5
617 617 $ echo foo > foo
618 618 $ hg add
619 619 adding foo
620 620 $ hg ci -m 'add foo' # rev 0
621 621
622 622 $ hg tag -r 0 -l localtag
623 623 $ hg tag --remove localtag
624 624 abort: tag 'localtag' is not a global tag
625 625 [255]
626 626 $
627 627 $ hg tag -r 0 globaltag
628 628 $ hg tag --remove -l globaltag
629 629 abort: tag 'globaltag' is not a local tag
630 630 [255]
631 631 $ hg tags -v
632 632 tip 1:a0b6fe111088
633 633 localtag 0:bbd179dfa0a7 local
634 634 globaltag 0:bbd179dfa0a7
635 635
636 636 Templated output:
637 637
638 638 (immediate values)
639 639
640 640 $ hg tags -T '{pad(tag, 9)} {rev}:{node} ({type})\n'
641 641 tip 1:a0b6fe111088c8c29567d3876cc466aa02927cae ()
642 642 localtag 0:bbd179dfa0a71671c253b3ae0aa1513b60d199fa (local)
643 643 globaltag 0:bbd179dfa0a71671c253b3ae0aa1513b60d199fa ()
644 644
645 645 (ctx/revcache dependent)
646 646
647 647 $ hg tags -T '{pad(tag, 9)} {rev} {file_adds}\n'
648 648 tip 1 .hgtags
649 649 localtag 0 foo
650 650 globaltag 0 foo
651 651
652 652 $ hg tags -T '{pad(tag, 9)} {rev}:{node|shortest}\n'
653 653 tip 1:a0b6
654 654 localtag 0:bbd1
655 655 globaltag 0:bbd1
656 656
657 657 Test for issue3911
658 658
659 659 $ hg tag -r 0 -l localtag2
660 660 $ hg tag -l --remove localtag2
661 661 $ hg tags -v
662 662 tip 1:a0b6fe111088
663 663 localtag 0:bbd179dfa0a7 local
664 664 globaltag 0:bbd179dfa0a7
665 665
666 666 $ hg tag -r 1 -f localtag
667 667 $ hg tags -v
668 668 tip 2:5c70a037bb37
669 669 localtag 1:a0b6fe111088
670 670 globaltag 0:bbd179dfa0a7
671 671
672 672 $ hg tags -v
673 673 tip 2:5c70a037bb37
674 674 localtag 1:a0b6fe111088
675 675 globaltag 0:bbd179dfa0a7
676 676
677 677 $ hg tag -r 1 localtag2
678 678 $ hg tags -v
679 679 tip 3:bbfb8cd42be2
680 680 localtag2 1:a0b6fe111088
681 681 localtag 1:a0b6fe111088
682 682 globaltag 0:bbd179dfa0a7
683 683
684 684 $ hg tags -v
685 685 tip 3:bbfb8cd42be2
686 686 localtag2 1:a0b6fe111088
687 687 localtag 1:a0b6fe111088
688 688 globaltag 0:bbd179dfa0a7
689 689
690 690 $ cd ..
691 691
692 692 Create a repository with tags data to test .hgtags fnodes transfer
693 693
694 694 $ hg init tagsserver
695 695 $ cd tagsserver
696 696 $ touch foo
697 697 $ hg -q commit -A -m initial
698 698 $ hg tag -m 'tag 0.1' 0.1
699 699 $ echo second > foo
700 700 $ hg commit -m second
701 701 $ hg tag -m 'tag 0.2' 0.2
702 702 $ hg tags
703 703 tip 3:40f0358cb314
704 704 0.2 2:f63cc8fe54e4
705 705 0.1 0:96ee1d7354c4
706 706 $ cd ..
707 707
708 708 Cloning should pull down hgtags fnodes mappings and write the cache file
709 709
710 710 $ hg clone --pull tagsserver tagsclient
711 711 requesting all changes
712 712 adding changesets
713 713 adding manifests
714 714 adding file changes
715 715 added 4 changesets with 4 changes to 2 files
716 716 new changesets 96ee1d7354c4:40f0358cb314
717 717 updating to branch default
718 718 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
719 719
720 720 Missing tags2* files means the cache wasn't written through the normal mechanism.
721 721
722 722 $ ls tagsclient/.hg/cache
723 723 branch2-base
724 724 hgtagsfnodes1
725 725 rbc-names-v1
726 726 rbc-revs-v1
727 727
728 728 Cache should contain the head only, even though other nodes have tags data
729 729
730 730 $ f --size --hexdump tagsclient/.hg/cache/hgtagsfnodes1
731 731 tagsclient/.hg/cache/hgtagsfnodes1: size=96
732 732 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
733 733 0010: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
734 734 0020: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
735 735 0030: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
736 736 0040: ff ff ff ff ff ff ff ff 40 f0 35 8c 19 e0 a7 d3 |........@.5.....|
737 737 0050: 8a 5c 6a 82 4d cf fb a5 87 d0 2f a3 1e 4f 2f 8a |.\j.M...../..O/.|
738 738
739 739 Running hg tags should produce tags2* file and not change cache
740 740
741 741 $ hg -R tagsclient tags
742 742 tip 3:40f0358cb314
743 743 0.2 2:f63cc8fe54e4
744 744 0.1 0:96ee1d7354c4
745 745
746 746 $ ls tagsclient/.hg/cache
747 747 branch2-base
748 748 hgtagsfnodes1
749 749 rbc-names-v1
750 750 rbc-revs-v1
751 751 tags2-visible
752 752
753 753 $ f --size --hexdump tagsclient/.hg/cache/hgtagsfnodes1
754 754 tagsclient/.hg/cache/hgtagsfnodes1: size=96
755 755 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
756 756 0010: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
757 757 0020: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
758 758 0030: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
759 759 0040: ff ff ff ff ff ff ff ff 40 f0 35 8c 19 e0 a7 d3 |........@.5.....|
760 760 0050: 8a 5c 6a 82 4d cf fb a5 87 d0 2f a3 1e 4f 2f 8a |.\j.M...../..O/.|
761 761
762 762 Check that the bundle includes cache data
763 763
764 764 $ hg -R tagsclient bundle --all ./test-cache-in-bundle-all-rev.hg
765 765 4 changesets found
766 766 $ hg debugbundle ./test-cache-in-bundle-all-rev.hg
767 767 Stream params: {Compression: BZ}
768 768 changegroup -- {nbchanges: 4, version: 02} (mandatory: True)
769 769 96ee1d7354c4ad7372047672c36a1f561e3a6a4c
770 770 c4dab0c2fd337eb9191f80c3024830a4889a8f34
771 771 f63cc8fe54e4d326f8d692805d70e092f851ddb1
772 772 40f0358cb314c824a5929ee527308d90e023bc10
773 773 hgtagsfnodes -- {} (mandatory: True)
774 774 cache:rev-branch-cache -- {} (mandatory: False)
775 775
776 776 Check that local clone includes cache data
777 777
778 778 $ hg clone tagsclient tags-local-clone
779 779 updating to branch default
780 780 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
781 781 $ (cd tags-local-clone/.hg/cache/; ls -1 tag*)
782 782 tags2-visible
783 783
784 784 Avoid writing logs on trying to delete an already deleted tag
785 785 $ hg init issue5752
786 786 $ cd issue5752
787 787 $ echo > a
788 788 $ hg commit -Am 'add a'
789 789 adding a
790 790 $ hg tag a
791 791 $ hg tags
792 792 tip 1:bd7ee4f3939b
793 793 a 0:a8a82d372bb3
794 794 $ hg log
795 795 changeset: 1:bd7ee4f3939b
796 796 tag: tip
797 797 user: test
798 798 date: Thu Jan 01 00:00:00 1970 +0000
799 799 summary: Added tag a for changeset a8a82d372bb3
800 800
801 801 changeset: 0:a8a82d372bb3
802 802 tag: a
803 803 user: test
804 804 date: Thu Jan 01 00:00:00 1970 +0000
805 805 summary: add a
806 806
807 807 $ hg tag --remove a
808 808 $ hg log
809 809 changeset: 2:e7feacc7ec9e
810 810 tag: tip
811 811 user: test
812 812 date: Thu Jan 01 00:00:00 1970 +0000
813 813 summary: Removed tag a
814 814
815 815 changeset: 1:bd7ee4f3939b
816 816 user: test
817 817 date: Thu Jan 01 00:00:00 1970 +0000
818 818 summary: Added tag a for changeset a8a82d372bb3
819 819
820 820 changeset: 0:a8a82d372bb3
821 821 user: test
822 822 date: Thu Jan 01 00:00:00 1970 +0000
823 823 summary: add a
824 824
825 825 $ hg tag --remove a
826 826 abort: tag 'a' is already removed
827 827 [255]
828 828 $ hg log
829 829 changeset: 2:e7feacc7ec9e
830 830 tag: tip
831 831 user: test
832 832 date: Thu Jan 01 00:00:00 1970 +0000
833 833 summary: Removed tag a
834 834
835 835 changeset: 1:bd7ee4f3939b
836 836 user: test
837 837 date: Thu Jan 01 00:00:00 1970 +0000
838 838 summary: Added tag a for changeset a8a82d372bb3
839 839
840 840 changeset: 0:a8a82d372bb3
841 841 user: test
842 842 date: Thu Jan 01 00:00:00 1970 +0000
843 843 summary: add a
844 844
845 845 $ cat .hgtags
846 846 a8a82d372bb35b42ff736e74f07c23bcd99c371f a
847 847 a8a82d372bb35b42ff736e74f07c23bcd99c371f a
848 848 0000000000000000000000000000000000000000 a
General Comments 0
You need to be logged in to leave comments. Login now