##// END OF EJS Templates
streamclone: treat volatile file as "fullfile"...
marmoute -
r47751:aed6ceaa default
parent child Browse files
Show More
@@ -1,799 +1,810 b''
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import functools
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from .pycompat import getattr
17 17 from .node import hex
18 18 from . import (
19 19 changelog,
20 20 error,
21 21 manifest,
22 22 policy,
23 23 pycompat,
24 24 util,
25 25 vfs as vfsmod,
26 26 )
27 27 from .utils import hashutil
28 28
29 29 parsers = policy.importmod('parsers')
30 30 # how much bytes should be read from fncache in one read
31 31 # It is done to prevent loading large fncache files into memory
32 32 fncache_chunksize = 10 ** 6
33 33
34 34
35 35 def _matchtrackedpath(path, matcher):
36 36 """parses a fncache entry and returns whether the entry is tracking a path
37 37 matched by matcher or not.
38 38
39 39 If matcher is None, returns True"""
40 40
41 41 if matcher is None:
42 42 return True
43 43 path = decodedir(path)
44 44 if path.startswith(b'data/'):
45 45 return matcher(path[len(b'data/') : -len(b'.i')])
46 46 elif path.startswith(b'meta/'):
47 47 return matcher.visitdir(path[len(b'meta/') : -len(b'/00manifest.i')])
48 48
49 49 raise error.ProgrammingError(b"cannot decode path %s" % path)
50 50
51 51
52 52 # This avoids a collision between a file named foo and a dir named
53 53 # foo.i or foo.d
54 54 def _encodedir(path):
55 55 """
56 56 >>> _encodedir(b'data/foo.i')
57 57 'data/foo.i'
58 58 >>> _encodedir(b'data/foo.i/bla.i')
59 59 'data/foo.i.hg/bla.i'
60 60 >>> _encodedir(b'data/foo.i.hg/bla.i')
61 61 'data/foo.i.hg.hg/bla.i'
62 62 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
63 63 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
64 64 """
65 65 return (
66 66 path.replace(b".hg/", b".hg.hg/")
67 67 .replace(b".i/", b".i.hg/")
68 68 .replace(b".d/", b".d.hg/")
69 69 )
70 70
71 71
72 72 encodedir = getattr(parsers, 'encodedir', _encodedir)
73 73
74 74
75 75 def decodedir(path):
76 76 """
77 77 >>> decodedir(b'data/foo.i')
78 78 'data/foo.i'
79 79 >>> decodedir(b'data/foo.i.hg/bla.i')
80 80 'data/foo.i/bla.i'
81 81 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
82 82 'data/foo.i.hg/bla.i'
83 83 """
84 84 if b".hg/" not in path:
85 85 return path
86 86 return (
87 87 path.replace(b".d.hg/", b".d/")
88 88 .replace(b".i.hg/", b".i/")
89 89 .replace(b".hg.hg/", b".hg/")
90 90 )
91 91
92 92
93 93 def _reserved():
94 94 """characters that are problematic for filesystems
95 95
96 96 * ascii escapes (0..31)
97 97 * ascii hi (126..255)
98 98 * windows specials
99 99
100 100 these characters will be escaped by encodefunctions
101 101 """
102 102 winreserved = [ord(x) for x in u'\\:*?"<>|']
103 103 for x in range(32):
104 104 yield x
105 105 for x in range(126, 256):
106 106 yield x
107 107 for x in winreserved:
108 108 yield x
109 109
110 110
111 111 def _buildencodefun():
112 112 """
113 113 >>> enc, dec = _buildencodefun()
114 114
115 115 >>> enc(b'nothing/special.txt')
116 116 'nothing/special.txt'
117 117 >>> dec(b'nothing/special.txt')
118 118 'nothing/special.txt'
119 119
120 120 >>> enc(b'HELLO')
121 121 '_h_e_l_l_o'
122 122 >>> dec(b'_h_e_l_l_o')
123 123 'HELLO'
124 124
125 125 >>> enc(b'hello:world?')
126 126 'hello~3aworld~3f'
127 127 >>> dec(b'hello~3aworld~3f')
128 128 'hello:world?'
129 129
130 130 >>> enc(b'the\\x07quick\\xADshot')
131 131 'the~07quick~adshot'
132 132 >>> dec(b'the~07quick~adshot')
133 133 'the\\x07quick\\xadshot'
134 134 """
135 135 e = b'_'
136 136 xchr = pycompat.bytechr
137 137 asciistr = list(map(xchr, range(127)))
138 138 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
139 139
140 140 cmap = {x: x for x in asciistr}
141 141 for x in _reserved():
142 142 cmap[xchr(x)] = b"~%02x" % x
143 143 for x in capitals + [ord(e)]:
144 144 cmap[xchr(x)] = e + xchr(x).lower()
145 145
146 146 dmap = {}
147 147 for k, v in pycompat.iteritems(cmap):
148 148 dmap[v] = k
149 149
150 150 def decode(s):
151 151 i = 0
152 152 while i < len(s):
153 153 for l in pycompat.xrange(1, 4):
154 154 try:
155 155 yield dmap[s[i : i + l]]
156 156 i += l
157 157 break
158 158 except KeyError:
159 159 pass
160 160 else:
161 161 raise KeyError
162 162
163 163 return (
164 164 lambda s: b''.join(
165 165 [cmap[s[c : c + 1]] for c in pycompat.xrange(len(s))]
166 166 ),
167 167 lambda s: b''.join(list(decode(s))),
168 168 )
169 169
170 170
171 171 _encodefname, _decodefname = _buildencodefun()
172 172
173 173
174 174 def encodefilename(s):
175 175 """
176 176 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
177 177 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
178 178 """
179 179 return _encodefname(encodedir(s))
180 180
181 181
182 182 def decodefilename(s):
183 183 """
184 184 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
185 185 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
186 186 """
187 187 return decodedir(_decodefname(s))
188 188
189 189
190 190 def _buildlowerencodefun():
191 191 """
192 192 >>> f = _buildlowerencodefun()
193 193 >>> f(b'nothing/special.txt')
194 194 'nothing/special.txt'
195 195 >>> f(b'HELLO')
196 196 'hello'
197 197 >>> f(b'hello:world?')
198 198 'hello~3aworld~3f'
199 199 >>> f(b'the\\x07quick\\xADshot')
200 200 'the~07quick~adshot'
201 201 """
202 202 xchr = pycompat.bytechr
203 203 cmap = {xchr(x): xchr(x) for x in pycompat.xrange(127)}
204 204 for x in _reserved():
205 205 cmap[xchr(x)] = b"~%02x" % x
206 206 for x in range(ord(b"A"), ord(b"Z") + 1):
207 207 cmap[xchr(x)] = xchr(x).lower()
208 208
209 209 def lowerencode(s):
210 210 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
211 211
212 212 return lowerencode
213 213
214 214
215 215 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
216 216
217 217 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
218 218 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
219 219 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
220 220
221 221
222 222 def _auxencode(path, dotencode):
223 223 """
224 224 Encodes filenames containing names reserved by Windows or which end in
225 225 period or space. Does not touch other single reserved characters c.
226 226 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
227 227 Additionally encodes space or period at the beginning, if dotencode is
228 228 True. Parameter path is assumed to be all lowercase.
229 229 A segment only needs encoding if a reserved name appears as a
230 230 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
231 231 doesn't need encoding.
232 232
233 233 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
234 234 >>> _auxencode(s.split(b'/'), True)
235 235 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
236 236 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
237 237 >>> _auxencode(s.split(b'/'), False)
238 238 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
239 239 >>> _auxencode([b'foo. '], True)
240 240 ['foo.~20']
241 241 >>> _auxencode([b' .foo'], True)
242 242 ['~20.foo']
243 243 """
244 244 for i, n in enumerate(path):
245 245 if not n:
246 246 continue
247 247 if dotencode and n[0] in b'. ':
248 248 n = b"~%02x" % ord(n[0:1]) + n[1:]
249 249 path[i] = n
250 250 else:
251 251 l = n.find(b'.')
252 252 if l == -1:
253 253 l = len(n)
254 254 if (l == 3 and n[:3] in _winres3) or (
255 255 l == 4
256 256 and n[3:4] <= b'9'
257 257 and n[3:4] >= b'1'
258 258 and n[:3] in _winres4
259 259 ):
260 260 # encode third letter ('aux' -> 'au~78')
261 261 ec = b"~%02x" % ord(n[2:3])
262 262 n = n[0:2] + ec + n[3:]
263 263 path[i] = n
264 264 if n[-1] in b'. ':
265 265 # encode last period or space ('foo...' -> 'foo..~2e')
266 266 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
267 267 return path
268 268
269 269
270 270 _maxstorepathlen = 120
271 271 _dirprefixlen = 8
272 272 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
273 273
274 274
275 275 def _hashencode(path, dotencode):
276 276 digest = hex(hashutil.sha1(path).digest())
277 277 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
278 278 parts = _auxencode(le, dotencode)
279 279 basename = parts[-1]
280 280 _root, ext = os.path.splitext(basename)
281 281 sdirs = []
282 282 sdirslen = 0
283 283 for p in parts[:-1]:
284 284 d = p[:_dirprefixlen]
285 285 if d[-1] in b'. ':
286 286 # Windows can't access dirs ending in period or space
287 287 d = d[:-1] + b'_'
288 288 if sdirslen == 0:
289 289 t = len(d)
290 290 else:
291 291 t = sdirslen + 1 + len(d)
292 292 if t > _maxshortdirslen:
293 293 break
294 294 sdirs.append(d)
295 295 sdirslen = t
296 296 dirs = b'/'.join(sdirs)
297 297 if len(dirs) > 0:
298 298 dirs += b'/'
299 299 res = b'dh/' + dirs + digest + ext
300 300 spaceleft = _maxstorepathlen - len(res)
301 301 if spaceleft > 0:
302 302 filler = basename[:spaceleft]
303 303 res = b'dh/' + dirs + filler + digest + ext
304 304 return res
305 305
306 306
307 307 def _hybridencode(path, dotencode):
308 308 """encodes path with a length limit
309 309
310 310 Encodes all paths that begin with 'data/', according to the following.
311 311
312 312 Default encoding (reversible):
313 313
314 314 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
315 315 characters are encoded as '~xx', where xx is the two digit hex code
316 316 of the character (see encodefilename).
317 317 Relevant path components consisting of Windows reserved filenames are
318 318 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
319 319
320 320 Hashed encoding (not reversible):
321 321
322 322 If the default-encoded path is longer than _maxstorepathlen, a
323 323 non-reversible hybrid hashing of the path is done instead.
324 324 This encoding uses up to _dirprefixlen characters of all directory
325 325 levels of the lowerencoded path, but not more levels than can fit into
326 326 _maxshortdirslen.
327 327 Then follows the filler followed by the sha digest of the full path.
328 328 The filler is the beginning of the basename of the lowerencoded path
329 329 (the basename is everything after the last path separator). The filler
330 330 is as long as possible, filling in characters from the basename until
331 331 the encoded path has _maxstorepathlen characters (or all chars of the
332 332 basename have been taken).
333 333 The extension (e.g. '.i' or '.d') is preserved.
334 334
335 335 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
336 336 encoding was used.
337 337 """
338 338 path = encodedir(path)
339 339 ef = _encodefname(path).split(b'/')
340 340 res = b'/'.join(_auxencode(ef, dotencode))
341 341 if len(res) > _maxstorepathlen:
342 342 res = _hashencode(path, dotencode)
343 343 return res
344 344
345 345
346 346 def _pathencode(path):
347 347 de = encodedir(path)
348 348 if len(path) > _maxstorepathlen:
349 349 return _hashencode(de, True)
350 350 ef = _encodefname(de).split(b'/')
351 351 res = b'/'.join(_auxencode(ef, True))
352 352 if len(res) > _maxstorepathlen:
353 353 return _hashencode(de, True)
354 354 return res
355 355
356 356
357 357 _pathencode = getattr(parsers, 'pathencode', _pathencode)
358 358
359 359
360 360 def _plainhybridencode(f):
361 361 return _hybridencode(f, False)
362 362
363 363
364 364 def _calcmode(vfs):
365 365 try:
366 366 # files in .hg/ will be created using this mode
367 367 mode = vfs.stat().st_mode
368 368 # avoid some useless chmods
369 369 if (0o777 & ~util.umask) == (0o777 & mode):
370 370 mode = None
371 371 except OSError:
372 372 mode = None
373 373 return mode
374 374
375 375
376 376 _data = [
377 377 b'bookmarks',
378 378 b'narrowspec',
379 379 b'data',
380 380 b'meta',
381 381 b'00manifest.d',
382 382 b'00manifest.i',
383 383 b'00changelog.d',
384 384 b'00changelog.i',
385 385 b'phaseroots',
386 386 b'obsstore',
387 387 b'requires',
388 388 ]
389 389
390 390 REVLOG_FILES_MAIN_EXT = (b'.i', b'i.tmpcensored')
391 391 REVLOG_FILES_OTHER_EXT = (b'.d', b'.n', b'.nd', b'd.tmpcensored')
392 # files that are "volatile" and might change between listing and streaming
393 #
394 # note: the ".nd" file are nodemap data and won't "change" but they might be
395 # deleted.
396 REVLOG_FILES_VOLATILE_EXT = (b'.n', b'.nd')
392 397
393 398
394 399 def is_revlog(f, kind, st):
395 400 if kind != stat.S_IFREG:
396 401 return None
397 402 return revlog_type(f)
398 403
399 404
400 405 def revlog_type(f):
401 406 if f.endswith(REVLOG_FILES_MAIN_EXT):
402 407 return FILEFLAGS_REVLOG_MAIN
403 408 elif f.endswith(REVLOG_FILES_OTHER_EXT):
404 return FILETYPE_FILELOG_OTHER
409 t = FILETYPE_FILELOG_OTHER
410 if f.endswith(REVLOG_FILES_VOLATILE_EXT):
411 t |= FILEFLAGS_VOLATILE
412 return t
405 413
406 414
407 415 # the file is part of changelog data
408 416 FILEFLAGS_CHANGELOG = 1 << 13
409 417 # the file is part of manifest data
410 418 FILEFLAGS_MANIFESTLOG = 1 << 12
411 419 # the file is part of filelog data
412 420 FILEFLAGS_FILELOG = 1 << 11
413 421 # file that are not directly part of a revlog
414 422 FILEFLAGS_OTHER = 1 << 10
415 423
416 424 # the main entry point for a revlog
417 425 FILEFLAGS_REVLOG_MAIN = 1 << 1
418 426 # a secondary file for a revlog
419 427 FILEFLAGS_REVLOG_OTHER = 1 << 0
420 428
429 # files that are "volatile" and might change between listing and streaming
430 FILEFLAGS_VOLATILE = 1 << 20
431
421 432 FILETYPE_CHANGELOG_MAIN = FILEFLAGS_CHANGELOG | FILEFLAGS_REVLOG_MAIN
422 433 FILETYPE_CHANGELOG_OTHER = FILEFLAGS_CHANGELOG | FILEFLAGS_REVLOG_OTHER
423 434 FILETYPE_MANIFESTLOG_MAIN = FILEFLAGS_MANIFESTLOG | FILEFLAGS_REVLOG_MAIN
424 435 FILETYPE_MANIFESTLOG_OTHER = FILEFLAGS_MANIFESTLOG | FILEFLAGS_REVLOG_OTHER
425 436 FILETYPE_FILELOG_MAIN = FILEFLAGS_FILELOG | FILEFLAGS_REVLOG_MAIN
426 437 FILETYPE_FILELOG_OTHER = FILEFLAGS_FILELOG | FILEFLAGS_REVLOG_OTHER
427 438 FILETYPE_OTHER = FILEFLAGS_OTHER
428 439
429 440
430 441 class basicstore(object):
431 442 '''base class for local repository stores'''
432 443
433 444 def __init__(self, path, vfstype):
434 445 vfs = vfstype(path)
435 446 self.path = vfs.base
436 447 self.createmode = _calcmode(vfs)
437 448 vfs.createmode = self.createmode
438 449 self.rawvfs = vfs
439 450 self.vfs = vfsmod.filtervfs(vfs, encodedir)
440 451 self.opener = self.vfs
441 452
442 453 def join(self, f):
443 454 return self.path + b'/' + encodedir(f)
444 455
445 456 def _walk(self, relpath, recurse):
446 457 '''yields (unencoded, encoded, size)'''
447 458 path = self.path
448 459 if relpath:
449 460 path += b'/' + relpath
450 461 striplen = len(self.path) + 1
451 462 l = []
452 463 if self.rawvfs.isdir(path):
453 464 visit = [path]
454 465 readdir = self.rawvfs.readdir
455 466 while visit:
456 467 p = visit.pop()
457 468 for f, kind, st in readdir(p, stat=True):
458 469 fp = p + b'/' + f
459 470 rl_type = is_revlog(f, kind, st)
460 471 if rl_type is not None:
461 472 n = util.pconvert(fp[striplen:])
462 473 l.append((rl_type, decodedir(n), n, st.st_size))
463 474 elif kind == stat.S_IFDIR and recurse:
464 475 visit.append(fp)
465 476 l.sort()
466 477 return l
467 478
468 479 def changelog(self, trypending, concurrencychecker=None):
469 480 return changelog.changelog(
470 481 self.vfs,
471 482 trypending=trypending,
472 483 concurrencychecker=concurrencychecker,
473 484 )
474 485
475 486 def manifestlog(self, repo, storenarrowmatch):
476 487 rootstore = manifest.manifestrevlog(repo.nodeconstants, self.vfs)
477 488 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
478 489
479 490 def datafiles(self, matcher=None):
480 491 files = self._walk(b'data', True) + self._walk(b'meta', True)
481 492 for (t, u, e, s) in files:
482 493 yield (FILEFLAGS_FILELOG | t, u, e, s)
483 494
484 495 def topfiles(self):
485 496 # yield manifest before changelog
486 497 files = reversed(self._walk(b'', False))
487 498 for (t, u, e, s) in files:
488 499 if u.startswith(b'00changelog'):
489 500 yield (FILEFLAGS_CHANGELOG | t, u, e, s)
490 501 elif u.startswith(b'00manifest'):
491 502 yield (FILEFLAGS_MANIFESTLOG | t, u, e, s)
492 503 else:
493 504 yield (FILETYPE_OTHER | t, u, e, s)
494 505
495 506 def walk(self, matcher=None):
496 507 """return file related to data storage (ie: revlogs)
497 508
498 509 yields (file_type, unencoded, encoded, size)
499 510
500 511 if a matcher is passed, storage files of only those tracked paths
501 512 are passed with matches the matcher
502 513 """
503 514 # yield data files first
504 515 for x in self.datafiles(matcher):
505 516 yield x
506 517 for x in self.topfiles():
507 518 yield x
508 519
509 520 def copylist(self):
510 521 return _data
511 522
512 523 def write(self, tr):
513 524 pass
514 525
515 526 def invalidatecaches(self):
516 527 pass
517 528
518 529 def markremoved(self, fn):
519 530 pass
520 531
521 532 def __contains__(self, path):
522 533 '''Checks if the store contains path'''
523 534 path = b"/".join((b"data", path))
524 535 # file?
525 536 if self.vfs.exists(path + b".i"):
526 537 return True
527 538 # dir?
528 539 if not path.endswith(b"/"):
529 540 path = path + b"/"
530 541 return self.vfs.exists(path)
531 542
532 543
533 544 class encodedstore(basicstore):
534 545 def __init__(self, path, vfstype):
535 546 vfs = vfstype(path + b'/store')
536 547 self.path = vfs.base
537 548 self.createmode = _calcmode(vfs)
538 549 vfs.createmode = self.createmode
539 550 self.rawvfs = vfs
540 551 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
541 552 self.opener = self.vfs
542 553
543 554 def datafiles(self, matcher=None):
544 555 for t, a, b, size in super(encodedstore, self).datafiles():
545 556 try:
546 557 a = decodefilename(a)
547 558 except KeyError:
548 559 a = None
549 560 if a is not None and not _matchtrackedpath(a, matcher):
550 561 continue
551 562 yield t, a, b, size
552 563
553 564 def join(self, f):
554 565 return self.path + b'/' + encodefilename(f)
555 566
556 567 def copylist(self):
557 568 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in _data]
558 569
559 570
560 571 class fncache(object):
561 572 # the filename used to be partially encoded
562 573 # hence the encodedir/decodedir dance
563 574 def __init__(self, vfs):
564 575 self.vfs = vfs
565 576 self.entries = None
566 577 self._dirty = False
567 578 # set of new additions to fncache
568 579 self.addls = set()
569 580
570 581 def ensureloaded(self, warn=None):
571 582 """read the fncache file if not already read.
572 583
573 584 If the file on disk is corrupted, raise. If warn is provided,
574 585 warn and keep going instead."""
575 586 if self.entries is None:
576 587 self._load(warn)
577 588
578 589 def _load(self, warn=None):
579 590 '''fill the entries from the fncache file'''
580 591 self._dirty = False
581 592 try:
582 593 fp = self.vfs(b'fncache', mode=b'rb')
583 594 except IOError:
584 595 # skip nonexistent file
585 596 self.entries = set()
586 597 return
587 598
588 599 self.entries = set()
589 600 chunk = b''
590 601 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
591 602 chunk += c
592 603 try:
593 604 p = chunk.rindex(b'\n')
594 605 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
595 606 chunk = chunk[p + 1 :]
596 607 except ValueError:
597 608 # substring '\n' not found, maybe the entry is bigger than the
598 609 # chunksize, so let's keep iterating
599 610 pass
600 611
601 612 if chunk:
602 613 msg = _(b"fncache does not ends with a newline")
603 614 if warn:
604 615 warn(msg + b'\n')
605 616 else:
606 617 raise error.Abort(
607 618 msg,
608 619 hint=_(
609 620 b"use 'hg debugrebuildfncache' to "
610 621 b"rebuild the fncache"
611 622 ),
612 623 )
613 624 self._checkentries(fp, warn)
614 625 fp.close()
615 626
616 627 def _checkentries(self, fp, warn):
617 628 """ make sure there is no empty string in entries """
618 629 if b'' in self.entries:
619 630 fp.seek(0)
620 631 for n, line in enumerate(util.iterfile(fp)):
621 632 if not line.rstrip(b'\n'):
622 633 t = _(b'invalid entry in fncache, line %d') % (n + 1)
623 634 if warn:
624 635 warn(t + b'\n')
625 636 else:
626 637 raise error.Abort(t)
627 638
628 639 def write(self, tr):
629 640 if self._dirty:
630 641 assert self.entries is not None
631 642 self.entries = self.entries | self.addls
632 643 self.addls = set()
633 644 tr.addbackup(b'fncache')
634 645 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
635 646 if self.entries:
636 647 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
637 648 fp.close()
638 649 self._dirty = False
639 650 if self.addls:
640 651 # if we have just new entries, let's append them to the fncache
641 652 tr.addbackup(b'fncache')
642 653 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
643 654 if self.addls:
644 655 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
645 656 fp.close()
646 657 self.entries = None
647 658 self.addls = set()
648 659
649 660 def add(self, fn):
650 661 if self.entries is None:
651 662 self._load()
652 663 if fn not in self.entries:
653 664 self.addls.add(fn)
654 665
655 666 def remove(self, fn):
656 667 if self.entries is None:
657 668 self._load()
658 669 if fn in self.addls:
659 670 self.addls.remove(fn)
660 671 return
661 672 try:
662 673 self.entries.remove(fn)
663 674 self._dirty = True
664 675 except KeyError:
665 676 pass
666 677
667 678 def __contains__(self, fn):
668 679 if fn in self.addls:
669 680 return True
670 681 if self.entries is None:
671 682 self._load()
672 683 return fn in self.entries
673 684
674 685 def __iter__(self):
675 686 if self.entries is None:
676 687 self._load()
677 688 return iter(self.entries | self.addls)
678 689
679 690
680 691 class _fncachevfs(vfsmod.proxyvfs):
681 692 def __init__(self, vfs, fnc, encode):
682 693 vfsmod.proxyvfs.__init__(self, vfs)
683 694 self.fncache = fnc
684 695 self.encode = encode
685 696
686 697 def __call__(self, path, mode=b'r', *args, **kw):
687 698 encoded = self.encode(path)
688 699 if mode not in (b'r', b'rb') and (
689 700 path.startswith(b'data/') or path.startswith(b'meta/')
690 701 ):
691 702 # do not trigger a fncache load when adding a file that already is
692 703 # known to exist.
693 704 notload = self.fncache.entries is None and self.vfs.exists(encoded)
694 705 if notload and b'a' in mode and not self.vfs.stat(encoded).st_size:
695 706 # when appending to an existing file, if the file has size zero,
696 707 # it should be considered as missing. Such zero-size files are
697 708 # the result of truncation when a transaction is aborted.
698 709 notload = False
699 710 if not notload:
700 711 self.fncache.add(path)
701 712 return self.vfs(encoded, mode, *args, **kw)
702 713
703 714 def join(self, path):
704 715 if path:
705 716 return self.vfs.join(self.encode(path))
706 717 else:
707 718 return self.vfs.join(path)
708 719
709 720
710 721 class fncachestore(basicstore):
711 722 def __init__(self, path, vfstype, dotencode):
712 723 if dotencode:
713 724 encode = _pathencode
714 725 else:
715 726 encode = _plainhybridencode
716 727 self.encode = encode
717 728 vfs = vfstype(path + b'/store')
718 729 self.path = vfs.base
719 730 self.pathsep = self.path + b'/'
720 731 self.createmode = _calcmode(vfs)
721 732 vfs.createmode = self.createmode
722 733 self.rawvfs = vfs
723 734 fnc = fncache(vfs)
724 735 self.fncache = fnc
725 736 self.vfs = _fncachevfs(vfs, fnc, encode)
726 737 self.opener = self.vfs
727 738
728 739 def join(self, f):
729 740 return self.pathsep + self.encode(f)
730 741
731 742 def getsize(self, path):
732 743 return self.rawvfs.stat(path).st_size
733 744
734 745 def datafiles(self, matcher=None):
735 746 for f in sorted(self.fncache):
736 747 if not _matchtrackedpath(f, matcher):
737 748 continue
738 749 ef = self.encode(f)
739 750 try:
740 751 t = revlog_type(f)
741 752 t |= FILEFLAGS_FILELOG
742 753 yield t, f, ef, self.getsize(ef)
743 754 except OSError as err:
744 755 if err.errno != errno.ENOENT:
745 756 raise
746 757
747 758 def copylist(self):
748 759 d = (
749 760 b'bookmarks',
750 761 b'narrowspec',
751 762 b'data',
752 763 b'meta',
753 764 b'dh',
754 765 b'fncache',
755 766 b'phaseroots',
756 767 b'obsstore',
757 768 b'00manifest.d',
758 769 b'00manifest.i',
759 770 b'00changelog.d',
760 771 b'00changelog.i',
761 772 b'requires',
762 773 )
763 774 return [b'requires', b'00changelog.i'] + [b'store/' + f for f in d]
764 775
765 776 def write(self, tr):
766 777 self.fncache.write(tr)
767 778
768 779 def invalidatecaches(self):
769 780 self.fncache.entries = None
770 781 self.fncache.addls = set()
771 782
772 783 def markremoved(self, fn):
773 784 self.fncache.remove(fn)
774 785
775 786 def _exists(self, f):
776 787 ef = self.encode(f)
777 788 try:
778 789 self.getsize(ef)
779 790 return True
780 791 except OSError as err:
781 792 if err.errno != errno.ENOENT:
782 793 raise
783 794 # nonexistent entry
784 795 return False
785 796
786 797 def __contains__(self, path):
787 798 '''Checks if the store contains path'''
788 799 path = b"/".join((b"data", path))
789 800 # check for files (exact match)
790 801 e = path + b'.i'
791 802 if e in self.fncache and self._exists(e):
792 803 return True
793 804 # now check for directories (prefix match)
794 805 if not path.endswith(b'/'):
795 806 path += b'/'
796 807 for e in self.fncache:
797 808 if e.startswith(path) and self._exists(e):
798 809 return True
799 810 return False
@@ -1,747 +1,750 b''
1 1 # streamclone.py - producing and consuming streaming repository data
2 2 #
3 3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import contextlib
11 11 import os
12 12 import struct
13 13
14 14 from .i18n import _
15 15 from .pycompat import open
16 16 from .interfaces import repository
17 17 from . import (
18 18 cacheutil,
19 19 error,
20 20 narrowspec,
21 21 phases,
22 22 pycompat,
23 23 requirements as requirementsmod,
24 24 scmutil,
25 25 store,
26 26 util,
27 27 )
28 28
29 29
30 30 def canperformstreamclone(pullop, bundle2=False):
31 31 """Whether it is possible to perform a streaming clone as part of pull.
32 32
33 33 ``bundle2`` will cause the function to consider stream clone through
34 34 bundle2 and only through bundle2.
35 35
36 36 Returns a tuple of (supported, requirements). ``supported`` is True if
37 37 streaming clone is supported and False otherwise. ``requirements`` is
38 38 a set of repo requirements from the remote, or ``None`` if stream clone
39 39 isn't supported.
40 40 """
41 41 repo = pullop.repo
42 42 remote = pullop.remote
43 43
44 44 bundle2supported = False
45 45 if pullop.canusebundle2:
46 46 if b'v2' in pullop.remotebundle2caps.get(b'stream', []):
47 47 bundle2supported = True
48 48 # else
49 49 # Server doesn't support bundle2 stream clone or doesn't support
50 50 # the versions we support. Fall back and possibly allow legacy.
51 51
52 52 # Ensures legacy code path uses available bundle2.
53 53 if bundle2supported and not bundle2:
54 54 return False, None
55 55 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
56 56 elif bundle2 and not bundle2supported:
57 57 return False, None
58 58
59 59 # Streaming clone only works on empty repositories.
60 60 if len(repo):
61 61 return False, None
62 62
63 63 # Streaming clone only works if all data is being requested.
64 64 if pullop.heads:
65 65 return False, None
66 66
67 67 streamrequested = pullop.streamclonerequested
68 68
69 69 # If we don't have a preference, let the server decide for us. This
70 70 # likely only comes into play in LANs.
71 71 if streamrequested is None:
72 72 # The server can advertise whether to prefer streaming clone.
73 73 streamrequested = remote.capable(b'stream-preferred')
74 74
75 75 if not streamrequested:
76 76 return False, None
77 77
78 78 # In order for stream clone to work, the client has to support all the
79 79 # requirements advertised by the server.
80 80 #
81 81 # The server advertises its requirements via the "stream" and "streamreqs"
82 82 # capability. "stream" (a value-less capability) is advertised if and only
83 83 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
84 84 # is advertised and contains a comma-delimited list of requirements.
85 85 requirements = set()
86 86 if remote.capable(b'stream'):
87 87 requirements.add(requirementsmod.REVLOGV1_REQUIREMENT)
88 88 else:
89 89 streamreqs = remote.capable(b'streamreqs')
90 90 # This is weird and shouldn't happen with modern servers.
91 91 if not streamreqs:
92 92 pullop.repo.ui.warn(
93 93 _(
94 94 b'warning: stream clone requested but server has them '
95 95 b'disabled\n'
96 96 )
97 97 )
98 98 return False, None
99 99
100 100 streamreqs = set(streamreqs.split(b','))
101 101 # Server requires something we don't support. Bail.
102 102 missingreqs = streamreqs - repo.supportedformats
103 103 if missingreqs:
104 104 pullop.repo.ui.warn(
105 105 _(
106 106 b'warning: stream clone requested but client is missing '
107 107 b'requirements: %s\n'
108 108 )
109 109 % b', '.join(sorted(missingreqs))
110 110 )
111 111 pullop.repo.ui.warn(
112 112 _(
113 113 b'(see https://www.mercurial-scm.org/wiki/MissingRequirement '
114 114 b'for more information)\n'
115 115 )
116 116 )
117 117 return False, None
118 118 requirements = streamreqs
119 119
120 120 return True, requirements
121 121
122 122
123 123 def maybeperformlegacystreamclone(pullop):
124 124 """Possibly perform a legacy stream clone operation.
125 125
126 126 Legacy stream clones are performed as part of pull but before all other
127 127 operations.
128 128
129 129 A legacy stream clone will not be performed if a bundle2 stream clone is
130 130 supported.
131 131 """
132 132 from . import localrepo
133 133
134 134 supported, requirements = canperformstreamclone(pullop)
135 135
136 136 if not supported:
137 137 return
138 138
139 139 repo = pullop.repo
140 140 remote = pullop.remote
141 141
142 142 # Save remote branchmap. We will use it later to speed up branchcache
143 143 # creation.
144 144 rbranchmap = None
145 145 if remote.capable(b'branchmap'):
146 146 with remote.commandexecutor() as e:
147 147 rbranchmap = e.callcommand(b'branchmap', {}).result()
148 148
149 149 repo.ui.status(_(b'streaming all changes\n'))
150 150
151 151 with remote.commandexecutor() as e:
152 152 fp = e.callcommand(b'stream_out', {}).result()
153 153
154 154 # TODO strictly speaking, this code should all be inside the context
155 155 # manager because the context manager is supposed to ensure all wire state
156 156 # is flushed when exiting. But the legacy peers don't do this, so it
157 157 # doesn't matter.
158 158 l = fp.readline()
159 159 try:
160 160 resp = int(l)
161 161 except ValueError:
162 162 raise error.ResponseError(
163 163 _(b'unexpected response from remote server:'), l
164 164 )
165 165 if resp == 1:
166 166 raise error.Abort(_(b'operation forbidden by server'))
167 167 elif resp == 2:
168 168 raise error.Abort(_(b'locking the remote repository failed'))
169 169 elif resp != 0:
170 170 raise error.Abort(_(b'the server sent an unknown error code'))
171 171
172 172 l = fp.readline()
173 173 try:
174 174 filecount, bytecount = map(int, l.split(b' ', 1))
175 175 except (ValueError, TypeError):
176 176 raise error.ResponseError(
177 177 _(b'unexpected response from remote server:'), l
178 178 )
179 179
180 180 with repo.lock():
181 181 consumev1(repo, fp, filecount, bytecount)
182 182
183 183 # new requirements = old non-format requirements +
184 184 # new format-related remote requirements
185 185 # requirements from the streamed-in repository
186 186 repo.requirements = requirements | (
187 187 repo.requirements - repo.supportedformats
188 188 )
189 189 repo.svfs.options = localrepo.resolvestorevfsoptions(
190 190 repo.ui, repo.requirements, repo.features
191 191 )
192 192 scmutil.writereporequirements(repo)
193 193
194 194 if rbranchmap:
195 195 repo._branchcaches.replace(repo, rbranchmap)
196 196
197 197 repo.invalidate()
198 198
199 199
200 200 def allowservergeneration(repo):
201 201 """Whether streaming clones are allowed from the server."""
202 202 if repository.REPO_FEATURE_STREAM_CLONE not in repo.features:
203 203 return False
204 204
205 205 if not repo.ui.configbool(b'server', b'uncompressed', untrusted=True):
206 206 return False
207 207
208 208 # The way stream clone works makes it impossible to hide secret changesets.
209 209 # So don't allow this by default.
210 210 secret = phases.hassecret(repo)
211 211 if secret:
212 212 return repo.ui.configbool(b'server', b'uncompressedallowsecret')
213 213
214 214 return True
215 215
216 216
217 217 # This is it's own function so extensions can override it.
218 218 def _walkstreamfiles(repo, matcher=None):
219 219 return repo.store.walk(matcher)
220 220
221 221
222 222 def generatev1(repo):
223 223 """Emit content for version 1 of a streaming clone.
224 224
225 225 This returns a 3-tuple of (file count, byte size, data iterator).
226 226
227 227 The data iterator consists of N entries for each file being transferred.
228 228 Each file entry starts as a line with the file name and integer size
229 229 delimited by a null byte.
230 230
231 231 The raw file data follows. Following the raw file data is the next file
232 232 entry, or EOF.
233 233
234 234 When used on the wire protocol, an additional line indicating protocol
235 235 success will be prepended to the stream. This function is not responsible
236 236 for adding it.
237 237
238 238 This function will obtain a repository lock to ensure a consistent view of
239 239 the store is captured. It therefore may raise LockError.
240 240 """
241 241 entries = []
242 242 total_bytes = 0
243 243 # Get consistent snapshot of repo, lock during scan.
244 244 with repo.lock():
245 245 repo.ui.debug(b'scanning\n')
246 246 for file_type, name, ename, size in _walkstreamfiles(repo):
247 247 if size:
248 248 entries.append((name, size))
249 249 total_bytes += size
250 250 _test_sync_point_walk_1(repo)
251 251 _test_sync_point_walk_2(repo)
252 252
253 253 repo.ui.debug(
254 254 b'%d files, %d bytes to transfer\n' % (len(entries), total_bytes)
255 255 )
256 256
257 257 svfs = repo.svfs
258 258 debugflag = repo.ui.debugflag
259 259
260 260 def emitrevlogdata():
261 261 for name, size in entries:
262 262 if debugflag:
263 263 repo.ui.debug(b'sending %s (%d bytes)\n' % (name, size))
264 264 # partially encode name over the wire for backwards compat
265 265 yield b'%s\0%d\n' % (store.encodedir(name), size)
266 266 # auditing at this stage is both pointless (paths are already
267 267 # trusted by the local repo) and expensive
268 268 with svfs(name, b'rb', auditpath=False) as fp:
269 269 if size <= 65536:
270 270 yield fp.read(size)
271 271 else:
272 272 for chunk in util.filechunkiter(fp, limit=size):
273 273 yield chunk
274 274
275 275 return len(entries), total_bytes, emitrevlogdata()
276 276
277 277
278 278 def generatev1wireproto(repo):
279 279 """Emit content for version 1 of streaming clone suitable for the wire.
280 280
281 281 This is the data output from ``generatev1()`` with 2 header lines. The
282 282 first line indicates overall success. The 2nd contains the file count and
283 283 byte size of payload.
284 284
285 285 The success line contains "0" for success, "1" for stream generation not
286 286 allowed, and "2" for error locking the repository (possibly indicating
287 287 a permissions error for the server process).
288 288 """
289 289 if not allowservergeneration(repo):
290 290 yield b'1\n'
291 291 return
292 292
293 293 try:
294 294 filecount, bytecount, it = generatev1(repo)
295 295 except error.LockError:
296 296 yield b'2\n'
297 297 return
298 298
299 299 # Indicates successful response.
300 300 yield b'0\n'
301 301 yield b'%d %d\n' % (filecount, bytecount)
302 302 for chunk in it:
303 303 yield chunk
304 304
305 305
306 306 def generatebundlev1(repo, compression=b'UN'):
307 307 """Emit content for version 1 of a stream clone bundle.
308 308
309 309 The first 4 bytes of the output ("HGS1") denote this as stream clone
310 310 bundle version 1.
311 311
312 312 The next 2 bytes indicate the compression type. Only "UN" is currently
313 313 supported.
314 314
315 315 The next 16 bytes are two 64-bit big endian unsigned integers indicating
316 316 file count and byte count, respectively.
317 317
318 318 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
319 319 of the requirements string, including a trailing \0. The following N bytes
320 320 are the requirements string, which is ASCII containing a comma-delimited
321 321 list of repo requirements that are needed to support the data.
322 322
323 323 The remaining content is the output of ``generatev1()`` (which may be
324 324 compressed in the future).
325 325
326 326 Returns a tuple of (requirements, data generator).
327 327 """
328 328 if compression != b'UN':
329 329 raise ValueError(b'we do not support the compression argument yet')
330 330
331 331 requirements = repo.requirements & repo.supportedformats
332 332 requires = b','.join(sorted(requirements))
333 333
334 334 def gen():
335 335 yield b'HGS1'
336 336 yield compression
337 337
338 338 filecount, bytecount, it = generatev1(repo)
339 339 repo.ui.status(
340 340 _(b'writing %d bytes for %d files\n') % (bytecount, filecount)
341 341 )
342 342
343 343 yield struct.pack(b'>QQ', filecount, bytecount)
344 344 yield struct.pack(b'>H', len(requires) + 1)
345 345 yield requires + b'\0'
346 346
347 347 # This is where we'll add compression in the future.
348 348 assert compression == b'UN'
349 349
350 350 progress = repo.ui.makeprogress(
351 351 _(b'bundle'), total=bytecount, unit=_(b'bytes')
352 352 )
353 353 progress.update(0)
354 354
355 355 for chunk in it:
356 356 progress.increment(step=len(chunk))
357 357 yield chunk
358 358
359 359 progress.complete()
360 360
361 361 return requirements, gen()
362 362
363 363
364 364 def consumev1(repo, fp, filecount, bytecount):
365 365 """Apply the contents from version 1 of a streaming clone file handle.
366 366
367 367 This takes the output from "stream_out" and applies it to the specified
368 368 repository.
369 369
370 370 Like "stream_out," the status line added by the wire protocol is not
371 371 handled by this function.
372 372 """
373 373 with repo.lock():
374 374 repo.ui.status(
375 375 _(b'%d files to transfer, %s of data\n')
376 376 % (filecount, util.bytecount(bytecount))
377 377 )
378 378 progress = repo.ui.makeprogress(
379 379 _(b'clone'), total=bytecount, unit=_(b'bytes')
380 380 )
381 381 progress.update(0)
382 382 start = util.timer()
383 383
384 384 # TODO: get rid of (potential) inconsistency
385 385 #
386 386 # If transaction is started and any @filecache property is
387 387 # changed at this point, it causes inconsistency between
388 388 # in-memory cached property and streamclone-ed file on the
389 389 # disk. Nested transaction prevents transaction scope "clone"
390 390 # below from writing in-memory changes out at the end of it,
391 391 # even though in-memory changes are discarded at the end of it
392 392 # regardless of transaction nesting.
393 393 #
394 394 # But transaction nesting can't be simply prohibited, because
395 395 # nesting occurs also in ordinary case (e.g. enabling
396 396 # clonebundles).
397 397
398 398 with repo.transaction(b'clone'):
399 399 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
400 400 for i in pycompat.xrange(filecount):
401 401 # XXX doesn't support '\n' or '\r' in filenames
402 402 l = fp.readline()
403 403 try:
404 404 name, size = l.split(b'\0', 1)
405 405 size = int(size)
406 406 except (ValueError, TypeError):
407 407 raise error.ResponseError(
408 408 _(b'unexpected response from remote server:'), l
409 409 )
410 410 if repo.ui.debugflag:
411 411 repo.ui.debug(
412 412 b'adding %s (%s)\n' % (name, util.bytecount(size))
413 413 )
414 414 # for backwards compat, name was partially encoded
415 415 path = store.decodedir(name)
416 416 with repo.svfs(path, b'w', backgroundclose=True) as ofp:
417 417 for chunk in util.filechunkiter(fp, limit=size):
418 418 progress.increment(step=len(chunk))
419 419 ofp.write(chunk)
420 420
421 421 # force @filecache properties to be reloaded from
422 422 # streamclone-ed file at next access
423 423 repo.invalidate(clearfilecache=True)
424 424
425 425 elapsed = util.timer() - start
426 426 if elapsed <= 0:
427 427 elapsed = 0.001
428 428 progress.complete()
429 429 repo.ui.status(
430 430 _(b'transferred %s in %.1f seconds (%s/sec)\n')
431 431 % (
432 432 util.bytecount(bytecount),
433 433 elapsed,
434 434 util.bytecount(bytecount / elapsed),
435 435 )
436 436 )
437 437
438 438
439 439 def readbundle1header(fp):
440 440 compression = fp.read(2)
441 441 if compression != b'UN':
442 442 raise error.Abort(
443 443 _(
444 444 b'only uncompressed stream clone bundles are '
445 445 b'supported; got %s'
446 446 )
447 447 % compression
448 448 )
449 449
450 450 filecount, bytecount = struct.unpack(b'>QQ', fp.read(16))
451 451 requireslen = struct.unpack(b'>H', fp.read(2))[0]
452 452 requires = fp.read(requireslen)
453 453
454 454 if not requires.endswith(b'\0'):
455 455 raise error.Abort(
456 456 _(
457 457 b'malformed stream clone bundle: '
458 458 b'requirements not properly encoded'
459 459 )
460 460 )
461 461
462 462 requirements = set(requires.rstrip(b'\0').split(b','))
463 463
464 464 return filecount, bytecount, requirements
465 465
466 466
467 467 def applybundlev1(repo, fp):
468 468 """Apply the content from a stream clone bundle version 1.
469 469
470 470 We assume the 4 byte header has been read and validated and the file handle
471 471 is at the 2 byte compression identifier.
472 472 """
473 473 if len(repo):
474 474 raise error.Abort(
475 475 _(b'cannot apply stream clone bundle on non-empty repo')
476 476 )
477 477
478 478 filecount, bytecount, requirements = readbundle1header(fp)
479 479 missingreqs = requirements - repo.supportedformats
480 480 if missingreqs:
481 481 raise error.Abort(
482 482 _(b'unable to apply stream clone: unsupported format: %s')
483 483 % b', '.join(sorted(missingreqs))
484 484 )
485 485
486 486 consumev1(repo, fp, filecount, bytecount)
487 487
488 488
489 489 class streamcloneapplier(object):
490 490 """Class to manage applying streaming clone bundles.
491 491
492 492 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
493 493 readers to perform bundle type-specific functionality.
494 494 """
495 495
496 496 def __init__(self, fh):
497 497 self._fh = fh
498 498
499 499 def apply(self, repo):
500 500 return applybundlev1(repo, self._fh)
501 501
502 502
503 503 # type of file to stream
504 504 _fileappend = 0 # append only file
505 505 _filefull = 1 # full snapshot file
506 506
507 507 # Source of the file
508 508 _srcstore = b's' # store (svfs)
509 509 _srccache = b'c' # cache (cache)
510 510
511 511 # This is it's own function so extensions can override it.
512 512 def _walkstreamfullstorefiles(repo):
513 513 """list snapshot file from the store"""
514 514 fnames = []
515 515 if not repo.publishing():
516 516 fnames.append(b'phaseroots')
517 517 return fnames
518 518
519 519
520 520 def _filterfull(entry, copy, vfsmap):
521 521 """actually copy the snapshot files"""
522 522 src, name, ftype, data = entry
523 523 if ftype != _filefull:
524 524 return entry
525 525 return (src, name, ftype, copy(vfsmap[src].join(name)))
526 526
527 527
528 528 @contextlib.contextmanager
529 529 def maketempcopies():
530 530 """return a function to temporary copy file"""
531 531 files = []
532 532 try:
533 533
534 534 def copy(src):
535 535 fd, dst = pycompat.mkstemp()
536 536 os.close(fd)
537 537 files.append(dst)
538 538 util.copyfiles(src, dst, hardlink=True)
539 539 return dst
540 540
541 541 yield copy
542 542 finally:
543 543 for tmp in files:
544 544 util.tryunlink(tmp)
545 545
546 546
547 547 def _makemap(repo):
548 548 """make a (src -> vfs) map for the repo"""
549 549 vfsmap = {
550 550 _srcstore: repo.svfs,
551 551 _srccache: repo.cachevfs,
552 552 }
553 553 # we keep repo.vfs out of the on purpose, ther are too many danger there
554 554 # (eg: .hg/hgrc)
555 555 assert repo.vfs not in vfsmap.values()
556 556
557 557 return vfsmap
558 558
559 559
560 560 def _emit2(repo, entries, totalfilesize):
561 561 """actually emit the stream bundle"""
562 562 vfsmap = _makemap(repo)
563 563 progress = repo.ui.makeprogress(
564 564 _(b'bundle'), total=totalfilesize, unit=_(b'bytes')
565 565 )
566 566 progress.update(0)
567 567 with maketempcopies() as copy, progress:
568 568 # copy is delayed until we are in the try
569 569 entries = [_filterfull(e, copy, vfsmap) for e in entries]
570 570 yield None # this release the lock on the repository
571 571 seen = 0
572 572
573 573 for src, name, ftype, data in entries:
574 574 vfs = vfsmap[src]
575 575 yield src
576 576 yield util.uvarintencode(len(name))
577 577 if ftype == _fileappend:
578 578 fp = vfs(name)
579 579 size = data
580 580 elif ftype == _filefull:
581 581 fp = open(data, b'rb')
582 582 size = util.fstat(fp).st_size
583 583 try:
584 584 yield util.uvarintencode(size)
585 585 yield name
586 586 if size <= 65536:
587 587 chunks = (fp.read(size),)
588 588 else:
589 589 chunks = util.filechunkiter(fp, limit=size)
590 590 for chunk in chunks:
591 591 seen += len(chunk)
592 592 progress.update(seen)
593 593 yield chunk
594 594 finally:
595 595 fp.close()
596 596
597 597
598 598 def _test_sync_point_walk_1(repo):
599 599 """a function for synchronisation during tests"""
600 600
601 601
602 602 def _test_sync_point_walk_2(repo):
603 603 """a function for synchronisation during tests"""
604 604
605 605
606 606 def generatev2(repo, includes, excludes, includeobsmarkers):
607 607 """Emit content for version 2 of a streaming clone.
608 608
609 609 the data stream consists the following entries:
610 610 1) A char representing the file destination (eg: store or cache)
611 611 2) A varint containing the length of the filename
612 612 3) A varint containing the length of file data
613 613 4) N bytes containing the filename (the internal, store-agnostic form)
614 614 5) N bytes containing the file data
615 615
616 616 Returns a 3-tuple of (file count, file size, data iterator).
617 617 """
618 618
619 619 with repo.lock():
620 620
621 621 entries = []
622 622 totalfilesize = 0
623 623
624 624 matcher = None
625 625 if includes or excludes:
626 626 matcher = narrowspec.match(repo.root, includes, excludes)
627 627
628 628 repo.ui.debug(b'scanning\n')
629 629 for rl_type, name, ename, size in _walkstreamfiles(repo, matcher):
630 630 if size:
631 entries.append((_srcstore, name, _fileappend, size))
631 ft = _fileappend
632 if rl_type & store.FILEFLAGS_VOLATILE:
633 ft = _filefull
634 entries.append((_srcstore, name, ft, size))
632 635 totalfilesize += size
633 636 for name in _walkstreamfullstorefiles(repo):
634 637 if repo.svfs.exists(name):
635 638 totalfilesize += repo.svfs.lstat(name).st_size
636 639 entries.append((_srcstore, name, _filefull, None))
637 640 if includeobsmarkers and repo.svfs.exists(b'obsstore'):
638 641 totalfilesize += repo.svfs.lstat(b'obsstore').st_size
639 642 entries.append((_srcstore, b'obsstore', _filefull, None))
640 643 for name in cacheutil.cachetocopy(repo):
641 644 if repo.cachevfs.exists(name):
642 645 totalfilesize += repo.cachevfs.lstat(name).st_size
643 646 entries.append((_srccache, name, _filefull, None))
644 647
645 648 chunks = _emit2(repo, entries, totalfilesize)
646 649 first = next(chunks)
647 650 assert first is None
648 651 _test_sync_point_walk_1(repo)
649 652 _test_sync_point_walk_2(repo)
650 653
651 654 return len(entries), totalfilesize, chunks
652 655
653 656
654 657 @contextlib.contextmanager
655 658 def nested(*ctxs):
656 659 this = ctxs[0]
657 660 rest = ctxs[1:]
658 661 with this:
659 662 if rest:
660 663 with nested(*rest):
661 664 yield
662 665 else:
663 666 yield
664 667
665 668
666 669 def consumev2(repo, fp, filecount, filesize):
667 670 """Apply the contents from a version 2 streaming clone.
668 671
669 672 Data is read from an object that only needs to provide a ``read(size)``
670 673 method.
671 674 """
672 675 with repo.lock():
673 676 repo.ui.status(
674 677 _(b'%d files to transfer, %s of data\n')
675 678 % (filecount, util.bytecount(filesize))
676 679 )
677 680
678 681 start = util.timer()
679 682 progress = repo.ui.makeprogress(
680 683 _(b'clone'), total=filesize, unit=_(b'bytes')
681 684 )
682 685 progress.update(0)
683 686
684 687 vfsmap = _makemap(repo)
685 688
686 689 with repo.transaction(b'clone'):
687 690 ctxs = (vfs.backgroundclosing(repo.ui) for vfs in vfsmap.values())
688 691 with nested(*ctxs):
689 692 for i in range(filecount):
690 693 src = util.readexactly(fp, 1)
691 694 vfs = vfsmap[src]
692 695 namelen = util.uvarintdecodestream(fp)
693 696 datalen = util.uvarintdecodestream(fp)
694 697
695 698 name = util.readexactly(fp, namelen)
696 699
697 700 if repo.ui.debugflag:
698 701 repo.ui.debug(
699 702 b'adding [%s] %s (%s)\n'
700 703 % (src, name, util.bytecount(datalen))
701 704 )
702 705
703 706 with vfs(name, b'w') as ofp:
704 707 for chunk in util.filechunkiter(fp, limit=datalen):
705 708 progress.increment(step=len(chunk))
706 709 ofp.write(chunk)
707 710
708 711 # force @filecache properties to be reloaded from
709 712 # streamclone-ed file at next access
710 713 repo.invalidate(clearfilecache=True)
711 714
712 715 elapsed = util.timer() - start
713 716 if elapsed <= 0:
714 717 elapsed = 0.001
715 718 repo.ui.status(
716 719 _(b'transferred %s in %.1f seconds (%s/sec)\n')
717 720 % (
718 721 util.bytecount(progress.pos),
719 722 elapsed,
720 723 util.bytecount(progress.pos / elapsed),
721 724 )
722 725 )
723 726 progress.complete()
724 727
725 728
726 729 def applybundlev2(repo, fp, filecount, filesize, requirements):
727 730 from . import localrepo
728 731
729 732 missingreqs = [r for r in requirements if r not in repo.supported]
730 733 if missingreqs:
731 734 raise error.Abort(
732 735 _(b'unable to apply stream clone: unsupported format: %s')
733 736 % b', '.join(sorted(missingreqs))
734 737 )
735 738
736 739 consumev2(repo, fp, filecount, filesize)
737 740
738 741 # new requirements = old non-format requirements +
739 742 # new format-related remote requirements
740 743 # requirements from the streamed-in repository
741 744 repo.requirements = set(requirements) | (
742 745 repo.requirements - repo.supportedformats
743 746 )
744 747 repo.svfs.options = localrepo.resolvestorevfsoptions(
745 748 repo.ui, repo.requirements, repo.features
746 749 )
747 750 scmutil.writereporequirements(repo)
@@ -1,1065 +1,1054 b''
1 1 ===================================
2 2 Test the persistent on-disk nodemap
3 3 ===================================
4 4
5 5
6 6 #if no-rust
7 7
8 8 $ cat << EOF >> $HGRCPATH
9 9 > [format]
10 10 > use-persistent-nodemap=yes
11 11 > [devel]
12 12 > persistent-nodemap=yes
13 13 > EOF
14 14
15 15 #endif
16 16
17 17 $ hg init test-repo --config storage.revlog.persistent-nodemap.slow-path=allow
18 18 $ cd test-repo
19 19
20 20 Check handling of the default slow-path value
21 21
22 22 #if no-pure no-rust
23 23
24 24 $ hg id
25 25 abort: accessing `persistent-nodemap` repository without associated fast implementation.
26 26 (check `hg help config.format.use-persistent-nodemap` for details)
27 27 [255]
28 28
29 29 Unlock further check (we are here to test the feature)
30 30
31 31 $ cat << EOF >> $HGRCPATH
32 32 > [storage]
33 33 > # to avoid spamming the test
34 34 > revlog.persistent-nodemap.slow-path=allow
35 35 > EOF
36 36
37 37 #endif
38 38
39 39 #if rust
40 40
41 41 Regression test for a previous bug in Rust/C FFI for the `Revlog_CAPI` capsule:
42 42 in places where `mercurial/cext/revlog.c` function signatures use `Py_ssize_t`
43 43 (64 bits on Linux x86_64), corresponding declarations in `rust/hg-cpython/src/cindex.rs`
44 44 incorrectly used `libc::c_int` (32 bits).
45 45 As a result, -1 passed from Rust for the null revision became 4294967295 in C.
46 46
47 47 $ hg log -r 00000000
48 48 changeset: -1:000000000000
49 49 tag: tip
50 50 user:
51 51 date: Thu Jan 01 00:00:00 1970 +0000
52 52
53 53
54 54 #endif
55 55
56 56
57 57 $ hg debugformat
58 58 format-variant repo
59 59 fncache: yes
60 60 dotencode: yes
61 61 generaldelta: yes
62 62 share-safe: no
63 63 sparserevlog: yes
64 64 persistent-nodemap: yes
65 65 copies-sdc: no
66 66 revlog-v2: no
67 67 plain-cl-delta: yes
68 68 compression: zlib (no-zstd !)
69 69 compression: zstd (zstd !)
70 70 compression-level: default
71 71 $ hg debugbuilddag .+5000 --new-file
72 72
73 73 $ hg debugnodemap --metadata
74 74 uid: ???????????????? (glob)
75 75 tip-rev: 5000
76 76 tip-node: 6b02b8c7b96654c25e86ba69eda198d7e6ad8b3c
77 77 data-length: 121088
78 78 data-unused: 0
79 79 data-unused: 0.000%
80 80 $ f --size .hg/store/00changelog.n
81 81 .hg/store/00changelog.n: size=70
82 82
83 83 Simple lookup works
84 84
85 85 $ ANYNODE=`hg log --template '{node|short}\n' --rev tip`
86 86 $ hg log -r "$ANYNODE" --template '{rev}\n'
87 87 5000
88 88
89 89
90 90 #if rust
91 91
92 92 $ f --sha256 .hg/store/00changelog-*.nd
93 93 .hg/store/00changelog-????????????????.nd: sha256=2e029d3200bd1a986b32784fc2ef1a3bd60dc331f025718bcf5ff44d93f026fd (glob)
94 94
95 95 $ f --sha256 .hg/store/00manifest-*.nd
96 96 .hg/store/00manifest-????????????????.nd: sha256=97117b1c064ea2f86664a124589e47db0e254e8d34739b5c5cc5bf31c9da2b51 (glob)
97 97 $ hg debugnodemap --dump-new | f --sha256 --size
98 98 size=121088, sha256=2e029d3200bd1a986b32784fc2ef1a3bd60dc331f025718bcf5ff44d93f026fd
99 99 $ hg debugnodemap --dump-disk | f --sha256 --bytes=256 --hexdump --size
100 100 size=121088, sha256=2e029d3200bd1a986b32784fc2ef1a3bd60dc331f025718bcf5ff44d93f026fd
101 101 0000: 00 00 00 91 00 00 00 20 00 00 00 bb 00 00 00 e7 |....... ........|
102 102 0010: 00 00 00 66 00 00 00 a1 00 00 01 13 00 00 01 22 |...f..........."|
103 103 0020: 00 00 00 23 00 00 00 fc 00 00 00 ba 00 00 00 5e |...#...........^|
104 104 0030: 00 00 00 df 00 00 01 4e 00 00 01 65 00 00 00 ab |.......N...e....|
105 105 0040: 00 00 00 a9 00 00 00 95 00 00 00 73 00 00 00 38 |...........s...8|
106 106 0050: 00 00 00 cc 00 00 00 92 00 00 00 90 00 00 00 69 |...............i|
107 107 0060: 00 00 00 ec 00 00 00 8d 00 00 01 4f 00 00 00 12 |...........O....|
108 108 0070: 00 00 02 0c 00 00 00 77 00 00 00 9c 00 00 00 8f |.......w........|
109 109 0080: 00 00 00 d5 00 00 00 6b 00 00 00 48 00 00 00 b3 |.......k...H....|
110 110 0090: 00 00 00 e5 00 00 00 b5 00 00 00 8e 00 00 00 ad |................|
111 111 00a0: 00 00 00 7b 00 00 00 7c 00 00 00 0b 00 00 00 2b |...{...|.......+|
112 112 00b0: 00 00 00 c6 00 00 00 1e 00 00 01 08 00 00 00 11 |................|
113 113 00c0: 00 00 01 30 00 00 00 26 00 00 01 9c 00 00 00 35 |...0...&.......5|
114 114 00d0: 00 00 00 b8 00 00 01 31 00 00 00 2c 00 00 00 55 |.......1...,...U|
115 115 00e0: 00 00 00 8a 00 00 00 9a 00 00 00 0c 00 00 01 1e |................|
116 116 00f0: 00 00 00 a4 00 00 00 83 00 00 00 c9 00 00 00 8c |................|
117 117
118 118
119 119 #else
120 120
121 121 $ f --sha256 .hg/store/00changelog-*.nd
122 122 .hg/store/00changelog-????????????????.nd: sha256=f544f5462ff46097432caf6d764091f6d8c46d6121be315ead8576d548c9dd79 (glob)
123 123 $ hg debugnodemap --dump-new | f --sha256 --size
124 124 size=121088, sha256=f544f5462ff46097432caf6d764091f6d8c46d6121be315ead8576d548c9dd79
125 125 $ hg debugnodemap --dump-disk | f --sha256 --bytes=256 --hexdump --size
126 126 size=121088, sha256=f544f5462ff46097432caf6d764091f6d8c46d6121be315ead8576d548c9dd79
127 127 0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
128 128 0010: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
129 129 0020: ff ff ff ff ff ff f5 06 ff ff ff ff ff ff f3 e7 |................|
130 130 0030: ff ff ef ca ff ff ff ff ff ff ff ff ff ff ff ff |................|
131 131 0040: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
132 132 0050: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ed 08 |................|
133 133 0060: ff ff ed 66 ff ff ff ff ff ff ff ff ff ff ff ff |...f............|
134 134 0070: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
135 135 0080: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
136 136 0090: ff ff ff ff ff ff ff ff ff ff ff ff ff ff f6 ed |................|
137 137 00a0: ff ff ff ff ff ff fe 61 ff ff ff ff ff ff ff ff |.......a........|
138 138 00b0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
139 139 00c0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
140 140 00d0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
141 141 00e0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff f1 02 |................|
142 142 00f0: ff ff ff ff ff ff ed 1b ff ff ff ff ff ff ff ff |................|
143 143
144 144 #endif
145 145
146 146 $ hg debugnodemap --check
147 147 revision in index: 5001
148 148 revision in nodemap: 5001
149 149
150 150 add a new commit
151 151
152 152 $ hg up
153 153 5001 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 154 $ echo foo > foo
155 155 $ hg add foo
156 156
157 157
158 158 Check slow-path config value handling
159 159 -------------------------------------
160 160
161 161 #if no-pure no-rust
162 162
163 163 $ hg id --config "storage.revlog.persistent-nodemap.slow-path=invalid-value"
164 164 unknown value for config "storage.revlog.persistent-nodemap.slow-path": "invalid-value"
165 165 falling back to default value: abort
166 166 abort: accessing `persistent-nodemap` repository without associated fast implementation.
167 167 (check `hg help config.format.use-persistent-nodemap` for details)
168 168 [255]
169 169
170 170 $ hg log -r . --config "storage.revlog.persistent-nodemap.slow-path=warn"
171 171 warning: accessing `persistent-nodemap` repository without associated fast implementation.
172 172 (check `hg help config.format.use-persistent-nodemap` for details)
173 173 changeset: 5000:6b02b8c7b966
174 174 tag: tip
175 175 user: debugbuilddag
176 176 date: Thu Jan 01 01:23:20 1970 +0000
177 177 summary: r5000
178 178
179 179 $ hg ci -m 'foo' --config "storage.revlog.persistent-nodemap.slow-path=abort"
180 180 abort: accessing `persistent-nodemap` repository without associated fast implementation.
181 181 (check `hg help config.format.use-persistent-nodemap` for details)
182 182 [255]
183 183
184 184 #else
185 185
186 186 $ hg id --config "storage.revlog.persistent-nodemap.slow-path=invalid-value"
187 187 unknown value for config "storage.revlog.persistent-nodemap.slow-path": "invalid-value"
188 188 falling back to default value: abort
189 189 6b02b8c7b966+ tip
190 190
191 191 #endif
192 192
193 193 $ hg ci -m 'foo'
194 194
195 195 #if no-pure no-rust
196 196 $ hg debugnodemap --metadata
197 197 uid: ???????????????? (glob)
198 198 tip-rev: 5001
199 199 tip-node: 16395c3cf7e231394735e6b1717823ada303fb0c
200 200 data-length: 121088
201 201 data-unused: 0
202 202 data-unused: 0.000%
203 203 #else
204 204 $ hg debugnodemap --metadata
205 205 uid: ???????????????? (glob)
206 206 tip-rev: 5001
207 207 tip-node: 16395c3cf7e231394735e6b1717823ada303fb0c
208 208 data-length: 121344
209 209 data-unused: 256
210 210 data-unused: 0.211%
211 211 #endif
212 212
213 213 $ f --size .hg/store/00changelog.n
214 214 .hg/store/00changelog.n: size=70
215 215
216 216 (The pure code use the debug code that perform incremental update, the C code reencode from scratch)
217 217
218 218 #if pure
219 219 $ f --sha256 .hg/store/00changelog-*.nd --size
220 220 .hg/store/00changelog-????????????????.nd: size=121344, sha256=cce54c5da5bde3ad72a4938673ed4064c86231b9c64376b082b163fdb20f8f66 (glob)
221 221 #endif
222 222
223 223 #if rust
224 224 $ f --sha256 .hg/store/00changelog-*.nd --size
225 225 .hg/store/00changelog-????????????????.nd: size=121344, sha256=952b042fcf614ceb37b542b1b723e04f18f83efe99bee4e0f5ccd232ef470e58 (glob)
226 226 #endif
227 227
228 228 #if no-pure no-rust
229 229 $ f --sha256 .hg/store/00changelog-*.nd --size
230 230 .hg/store/00changelog-????????????????.nd: size=121088, sha256=df7c06a035b96cb28c7287d349d603baef43240be7736fe34eea419a49702e17 (glob)
231 231 #endif
232 232
233 233 $ hg debugnodemap --check
234 234 revision in index: 5002
235 235 revision in nodemap: 5002
236 236
237 237 Test code path without mmap
238 238 ---------------------------
239 239
240 240 $ echo bar > bar
241 241 $ hg add bar
242 242 $ hg ci -m 'bar' --config storage.revlog.persistent-nodemap.mmap=no
243 243
244 244 $ hg debugnodemap --check --config storage.revlog.persistent-nodemap.mmap=yes
245 245 revision in index: 5003
246 246 revision in nodemap: 5003
247 247 $ hg debugnodemap --check --config storage.revlog.persistent-nodemap.mmap=no
248 248 revision in index: 5003
249 249 revision in nodemap: 5003
250 250
251 251
252 252 #if pure
253 253 $ hg debugnodemap --metadata
254 254 uid: ???????????????? (glob)
255 255 tip-rev: 5002
256 256 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
257 257 data-length: 121600
258 258 data-unused: 512
259 259 data-unused: 0.421%
260 260 $ f --sha256 .hg/store/00changelog-*.nd --size
261 261 .hg/store/00changelog-????????????????.nd: size=121600, sha256=def52503d049ccb823974af313a98a935319ba61f40f3aa06a8be4d35c215054 (glob)
262 262 #endif
263 263 #if rust
264 264 $ hg debugnodemap --metadata
265 265 uid: ???????????????? (glob)
266 266 tip-rev: 5002
267 267 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
268 268 data-length: 121600
269 269 data-unused: 512
270 270 data-unused: 0.421%
271 271 $ f --sha256 .hg/store/00changelog-*.nd --size
272 272 .hg/store/00changelog-????????????????.nd: size=121600, sha256=dacf5b5f1d4585fee7527d0e67cad5b1ba0930e6a0928f650f779aefb04ce3fb (glob)
273 273 #endif
274 274 #if no-pure no-rust
275 275 $ hg debugnodemap --metadata
276 276 uid: ???????????????? (glob)
277 277 tip-rev: 5002
278 278 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
279 279 data-length: 121088
280 280 data-unused: 0
281 281 data-unused: 0.000%
282 282 $ f --sha256 .hg/store/00changelog-*.nd --size
283 283 .hg/store/00changelog-????????????????.nd: size=121088, sha256=59fcede3e3cc587755916ceed29e3c33748cd1aa7d2f91828ac83e7979d935e8 (glob)
284 284 #endif
285 285
286 286 Test force warming the cache
287 287
288 288 $ rm .hg/store/00changelog.n
289 289 $ hg debugnodemap --metadata
290 290 $ hg debugupdatecache
291 291 #if pure
292 292 $ hg debugnodemap --metadata
293 293 uid: ???????????????? (glob)
294 294 tip-rev: 5002
295 295 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
296 296 data-length: 121088
297 297 data-unused: 0
298 298 data-unused: 0.000%
299 299 #else
300 300 $ hg debugnodemap --metadata
301 301 uid: ???????????????? (glob)
302 302 tip-rev: 5002
303 303 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
304 304 data-length: 121088
305 305 data-unused: 0
306 306 data-unused: 0.000%
307 307 #endif
308 308
309 309 Check out of sync nodemap
310 310 =========================
311 311
312 312 First copy old data on the side.
313 313
314 314 $ mkdir ../tmp-copies
315 315 $ cp .hg/store/00changelog-????????????????.nd .hg/store/00changelog.n ../tmp-copies
316 316
317 317 Nodemap lagging behind
318 318 ----------------------
319 319
320 320 make a new commit
321 321
322 322 $ echo bar2 > bar
323 323 $ hg ci -m 'bar2'
324 324 $ NODE=`hg log -r tip -T '{node}\n'`
325 325 $ hg log -r "$NODE" -T '{rev}\n'
326 326 5003
327 327
328 328 If the nodemap is lagging behind, it can catch up fine
329 329
330 330 $ hg debugnodemap --metadata
331 331 uid: ???????????????? (glob)
332 332 tip-rev: 5003
333 333 tip-node: c9329770f979ade2d16912267c38ba5f82fd37b3
334 334 data-length: 121344 (pure !)
335 335 data-length: 121344 (rust !)
336 336 data-length: 121152 (no-rust no-pure !)
337 337 data-unused: 192 (pure !)
338 338 data-unused: 192 (rust !)
339 339 data-unused: 0 (no-rust no-pure !)
340 340 data-unused: 0.158% (pure !)
341 341 data-unused: 0.158% (rust !)
342 342 data-unused: 0.000% (no-rust no-pure !)
343 343 $ cp -f ../tmp-copies/* .hg/store/
344 344 $ hg debugnodemap --metadata
345 345 uid: ???????????????? (glob)
346 346 tip-rev: 5002
347 347 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
348 348 data-length: 121088
349 349 data-unused: 0
350 350 data-unused: 0.000%
351 351 $ hg log -r "$NODE" -T '{rev}\n'
352 352 5003
353 353
354 354 changelog altered
355 355 -----------------
356 356
357 357 If the nodemap is not gated behind a requirements, an unaware client can alter
358 358 the repository so the revlog used to generate the nodemap is not longer
359 359 compatible with the persistent nodemap. We need to detect that.
360 360
361 361 $ hg up "$NODE~5"
362 362 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
363 363 $ echo bar > babar
364 364 $ hg add babar
365 365 $ hg ci -m 'babar'
366 366 created new head
367 367 $ OTHERNODE=`hg log -r tip -T '{node}\n'`
368 368 $ hg log -r "$OTHERNODE" -T '{rev}\n'
369 369 5004
370 370
371 371 $ hg --config extensions.strip= strip --rev "$NODE~1" --no-backup
372 372
373 373 the nodemap should detect the changelog have been tampered with and recover.
374 374
375 375 $ hg debugnodemap --metadata
376 376 uid: ???????????????? (glob)
377 377 tip-rev: 5002
378 378 tip-node: b355ef8adce0949b8bdf6afc72ca853740d65944
379 379 data-length: 121536 (pure !)
380 380 data-length: 121088 (rust !)
381 381 data-length: 121088 (no-pure no-rust !)
382 382 data-unused: 448 (pure !)
383 383 data-unused: 0 (rust !)
384 384 data-unused: 0 (no-pure no-rust !)
385 385 data-unused: 0.000% (rust !)
386 386 data-unused: 0.369% (pure !)
387 387 data-unused: 0.000% (no-pure no-rust !)
388 388
389 389 $ cp -f ../tmp-copies/* .hg/store/
390 390 $ hg debugnodemap --metadata
391 391 uid: ???????????????? (glob)
392 392 tip-rev: 5002
393 393 tip-node: 880b18d239dfa9f632413a2071bfdbcc4806a4fd
394 394 data-length: 121088
395 395 data-unused: 0
396 396 data-unused: 0.000%
397 397 $ hg log -r "$OTHERNODE" -T '{rev}\n'
398 398 5002
399 399
400 400 missing data file
401 401 -----------------
402 402
403 403 $ UUID=`hg debugnodemap --metadata| grep 'uid:' | \
404 404 > sed 's/uid: //'`
405 405 $ FILE=.hg/store/00changelog-"${UUID}".nd
406 406 $ mv $FILE ../tmp-data-file
407 407 $ cp .hg/store/00changelog.n ../tmp-docket
408 408
409 409 mercurial don't crash
410 410
411 411 $ hg log -r .
412 412 changeset: 5002:b355ef8adce0
413 413 tag: tip
414 414 parent: 4998:d918ad6d18d3
415 415 user: test
416 416 date: Thu Jan 01 00:00:00 1970 +0000
417 417 summary: babar
418 418
419 419 $ hg debugnodemap --metadata
420 420
421 421 $ hg debugupdatecache
422 422 $ hg debugnodemap --metadata
423 423 uid: * (glob)
424 424 tip-rev: 5002
425 425 tip-node: b355ef8adce0949b8bdf6afc72ca853740d65944
426 426 data-length: 121088
427 427 data-unused: 0
428 428 data-unused: 0.000%
429 429 $ mv ../tmp-data-file $FILE
430 430 $ mv ../tmp-docket .hg/store/00changelog.n
431 431
432 432 Check transaction related property
433 433 ==================================
434 434
435 435 An up to date nodemap should be available to shell hooks,
436 436
437 437 $ echo dsljfl > a
438 438 $ hg add a
439 439 $ hg ci -m a
440 440 $ hg debugnodemap --metadata
441 441 uid: ???????????????? (glob)
442 442 tip-rev: 5003
443 443 tip-node: a52c5079765b5865d97b993b303a18740113bbb2
444 444 data-length: 121088
445 445 data-unused: 0
446 446 data-unused: 0.000%
447 447 $ echo babar2 > babar
448 448 $ hg ci -m 'babar2' --config "hooks.pretxnclose.nodemap-test=hg debugnodemap --metadata"
449 449 uid: ???????????????? (glob)
450 450 tip-rev: 5004
451 451 tip-node: 2f5fb1c06a16834c5679d672e90da7c5f3b1a984
452 452 data-length: 121280 (pure !)
453 453 data-length: 121280 (rust !)
454 454 data-length: 121088 (no-pure no-rust !)
455 455 data-unused: 192 (pure !)
456 456 data-unused: 192 (rust !)
457 457 data-unused: 0 (no-pure no-rust !)
458 458 data-unused: 0.158% (pure !)
459 459 data-unused: 0.158% (rust !)
460 460 data-unused: 0.000% (no-pure no-rust !)
461 461 $ hg debugnodemap --metadata
462 462 uid: ???????????????? (glob)
463 463 tip-rev: 5004
464 464 tip-node: 2f5fb1c06a16834c5679d672e90da7c5f3b1a984
465 465 data-length: 121280 (pure !)
466 466 data-length: 121280 (rust !)
467 467 data-length: 121088 (no-pure no-rust !)
468 468 data-unused: 192 (pure !)
469 469 data-unused: 192 (rust !)
470 470 data-unused: 0 (no-pure no-rust !)
471 471 data-unused: 0.158% (pure !)
472 472 data-unused: 0.158% (rust !)
473 473 data-unused: 0.000% (no-pure no-rust !)
474 474
475 475 Another process does not see the pending nodemap content during run.
476 476
477 477 $ PATH=$RUNTESTDIR/testlib/:$PATH
478 478 $ echo qpoasp > a
479 479 $ hg ci -m a2 \
480 480 > --config "hooks.pretxnclose=wait-on-file 20 sync-repo-read sync-txn-pending" \
481 481 > --config "hooks.txnclose=touch sync-txn-close" > output.txt 2>&1 &
482 482
483 483 (read the repository while the commit transaction is pending)
484 484
485 485 $ wait-on-file 20 sync-txn-pending && \
486 486 > hg debugnodemap --metadata && \
487 487 > wait-on-file 20 sync-txn-close sync-repo-read
488 488 uid: ???????????????? (glob)
489 489 tip-rev: 5004
490 490 tip-node: 2f5fb1c06a16834c5679d672e90da7c5f3b1a984
491 491 data-length: 121280 (pure !)
492 492 data-length: 121280 (rust !)
493 493 data-length: 121088 (no-pure no-rust !)
494 494 data-unused: 192 (pure !)
495 495 data-unused: 192 (rust !)
496 496 data-unused: 0 (no-pure no-rust !)
497 497 data-unused: 0.158% (pure !)
498 498 data-unused: 0.158% (rust !)
499 499 data-unused: 0.000% (no-pure no-rust !)
500 500 $ hg debugnodemap --metadata
501 501 uid: ???????????????? (glob)
502 502 tip-rev: 5005
503 503 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
504 504 data-length: 121536 (pure !)
505 505 data-length: 121536 (rust !)
506 506 data-length: 121088 (no-pure no-rust !)
507 507 data-unused: 448 (pure !)
508 508 data-unused: 448 (rust !)
509 509 data-unused: 0 (no-pure no-rust !)
510 510 data-unused: 0.369% (pure !)
511 511 data-unused: 0.369% (rust !)
512 512 data-unused: 0.000% (no-pure no-rust !)
513 513
514 514 $ cat output.txt
515 515
516 516 Check that a failing transaction will properly revert the data
517 517
518 518 $ echo plakfe > a
519 519 $ f --size --sha256 .hg/store/00changelog-*.nd
520 520 .hg/store/00changelog-????????????????.nd: size=121536, sha256=bb414468d225cf52d69132e1237afba34d4346ee2eb81b505027e6197b107f03 (glob) (pure !)
521 521 .hg/store/00changelog-????????????????.nd: size=121536, sha256=909ac727bc4d1c0fda5f7bff3c620c98bd4a2967c143405a1503439e33b377da (glob) (rust !)
522 522 .hg/store/00changelog-????????????????.nd: size=121088, sha256=342d36d30d86dde67d3cb6c002606c4a75bcad665595d941493845066d9c8ee0 (glob) (no-pure no-rust !)
523 523 $ hg ci -m a3 --config "extensions.abort=$RUNTESTDIR/testlib/crash_transaction_late.py"
524 524 transaction abort!
525 525 rollback completed
526 526 abort: This is a late abort
527 527 [255]
528 528 $ hg debugnodemap --metadata
529 529 uid: ???????????????? (glob)
530 530 tip-rev: 5005
531 531 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
532 532 data-length: 121536 (pure !)
533 533 data-length: 121536 (rust !)
534 534 data-length: 121088 (no-pure no-rust !)
535 535 data-unused: 448 (pure !)
536 536 data-unused: 448 (rust !)
537 537 data-unused: 0 (no-pure no-rust !)
538 538 data-unused: 0.369% (pure !)
539 539 data-unused: 0.369% (rust !)
540 540 data-unused: 0.000% (no-pure no-rust !)
541 541 $ f --size --sha256 .hg/store/00changelog-*.nd
542 542 .hg/store/00changelog-????????????????.nd: size=121536, sha256=bb414468d225cf52d69132e1237afba34d4346ee2eb81b505027e6197b107f03 (glob) (pure !)
543 543 .hg/store/00changelog-????????????????.nd: size=121536, sha256=909ac727bc4d1c0fda5f7bff3c620c98bd4a2967c143405a1503439e33b377da (glob) (rust !)
544 544 .hg/store/00changelog-????????????????.nd: size=121088, sha256=342d36d30d86dde67d3cb6c002606c4a75bcad665595d941493845066d9c8ee0 (glob) (no-pure no-rust !)
545 545
546 546 Check that removing content does not confuse the nodemap
547 547 --------------------------------------------------------
548 548
549 549 removing data with rollback
550 550
551 551 $ echo aso > a
552 552 $ hg ci -m a4
553 553 $ hg rollback
554 554 repository tip rolled back to revision 5005 (undo commit)
555 555 working directory now based on revision 5005
556 556 $ hg id -r .
557 557 90d5d3ba2fc4 tip
558 558
559 559 roming data with strip
560 560
561 561 $ echo aso > a
562 562 $ hg ci -m a4
563 563 $ hg --config extensions.strip= strip -r . --no-backup
564 564 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
565 565 $ hg id -r . --traceback
566 566 90d5d3ba2fc4 tip
567 567
568 568 Test upgrade / downgrade
569 569 ========================
570 570
571 571 downgrading
572 572
573 573 $ cat << EOF >> .hg/hgrc
574 574 > [format]
575 575 > use-persistent-nodemap=no
576 576 > EOF
577 577 $ hg debugformat -v
578 578 format-variant repo config default
579 579 fncache: yes yes yes
580 580 dotencode: yes yes yes
581 581 generaldelta: yes yes yes
582 582 share-safe: no no no
583 583 sparserevlog: yes yes yes
584 584 persistent-nodemap: yes no no
585 585 copies-sdc: no no no
586 586 revlog-v2: no no no
587 587 plain-cl-delta: yes yes yes
588 588 compression: zlib zlib zlib (no-zstd !)
589 589 compression: zstd zstd zstd (zstd !)
590 590 compression-level: default default default
591 591 $ hg debugupgraderepo --run --no-backup
592 592 upgrade will perform the following actions:
593 593
594 594 requirements
595 595 preserved: dotencode, fncache, generaldelta, revlogv1, sparserevlog, store (no-zstd !)
596 596 preserved: dotencode, fncache, generaldelta, revlog-compression-zstd, revlogv1, sparserevlog, store (zstd !)
597 597 removed: persistent-nodemap
598 598
599 599 processed revlogs:
600 600 - all-filelogs
601 601 - changelog
602 602 - manifest
603 603
604 604 beginning upgrade...
605 605 repository locked and read-only
606 606 creating temporary repository to stage upgraded data: $TESTTMP/test-repo/.hg/upgrade.* (glob)
607 607 (it is safe to interrupt this process any time before data migration completes)
608 608 downgrading repository to not use persistent nodemap feature
609 609 removing temporary repository $TESTTMP/test-repo/.hg/upgrade.* (glob)
610 610 $ ls -1 .hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
611 611 00changelog-*.nd (glob)
612 612 00manifest-*.nd (glob)
613 613 undo.backup.00changelog.n
614 614 undo.backup.00manifest.n
615 615 $ hg debugnodemap --metadata
616 616
617 617
618 618 upgrading
619 619
620 620 $ cat << EOF >> .hg/hgrc
621 621 > [format]
622 622 > use-persistent-nodemap=yes
623 623 > EOF
624 624 $ hg debugformat -v
625 625 format-variant repo config default
626 626 fncache: yes yes yes
627 627 dotencode: yes yes yes
628 628 generaldelta: yes yes yes
629 629 share-safe: no no no
630 630 sparserevlog: yes yes yes
631 631 persistent-nodemap: no yes no
632 632 copies-sdc: no no no
633 633 revlog-v2: no no no
634 634 plain-cl-delta: yes yes yes
635 635 compression: zlib zlib zlib (no-zstd !)
636 636 compression: zstd zstd zstd (zstd !)
637 637 compression-level: default default default
638 638 $ hg debugupgraderepo --run --no-backup
639 639 upgrade will perform the following actions:
640 640
641 641 requirements
642 642 preserved: dotencode, fncache, generaldelta, revlogv1, sparserevlog, store (no-zstd !)
643 643 preserved: dotencode, fncache, generaldelta, revlog-compression-zstd, revlogv1, sparserevlog, store (zstd !)
644 644 added: persistent-nodemap
645 645
646 646 persistent-nodemap
647 647 Speedup revision lookup by node id.
648 648
649 649 processed revlogs:
650 650 - all-filelogs
651 651 - changelog
652 652 - manifest
653 653
654 654 beginning upgrade...
655 655 repository locked and read-only
656 656 creating temporary repository to stage upgraded data: $TESTTMP/test-repo/.hg/upgrade.* (glob)
657 657 (it is safe to interrupt this process any time before data migration completes)
658 658 upgrading repository to use persistent nodemap feature
659 659 removing temporary repository $TESTTMP/test-repo/.hg/upgrade.* (glob)
660 660 $ ls -1 .hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
661 661 00changelog-*.nd (glob)
662 662 00changelog.n
663 663 00manifest-*.nd (glob)
664 664 00manifest.n
665 665 undo.backup.00changelog.n
666 666 undo.backup.00manifest.n
667 667
668 668 $ hg debugnodemap --metadata
669 669 uid: * (glob)
670 670 tip-rev: 5005
671 671 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
672 672 data-length: 121088
673 673 data-unused: 0
674 674 data-unused: 0.000%
675 675
676 676 Running unrelated upgrade
677 677
678 678 $ hg debugupgraderepo --run --no-backup --quiet --optimize re-delta-all
679 679 upgrade will perform the following actions:
680 680
681 681 requirements
682 682 preserved: dotencode, fncache, generaldelta, persistent-nodemap, revlogv1, sparserevlog, store (no-zstd !)
683 683 preserved: dotencode, fncache, generaldelta, persistent-nodemap, revlog-compression-zstd, revlogv1, sparserevlog, store (zstd !)
684 684
685 685 optimisations: re-delta-all
686 686
687 687 processed revlogs:
688 688 - all-filelogs
689 689 - changelog
690 690 - manifest
691 691
692 692 $ ls -1 .hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
693 693 00changelog-*.nd (glob)
694 694 00changelog.n
695 695 00manifest-*.nd (glob)
696 696 00manifest.n
697 697
698 698 $ hg debugnodemap --metadata
699 699 uid: * (glob)
700 700 tip-rev: 5005
701 701 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
702 702 data-length: 121088
703 703 data-unused: 0
704 704 data-unused: 0.000%
705 705
706 706 Persistent nodemap and local/streaming clone
707 707 ============================================
708 708
709 709 $ cd ..
710 710
711 711 standard clone
712 712 --------------
713 713
714 714 The persistent nodemap should exist after a streaming clone
715 715
716 716 $ hg clone --pull --quiet -U test-repo standard-clone
717 717 $ ls -1 standard-clone/.hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
718 718 00changelog-*.nd (glob)
719 719 00changelog.n
720 720 00manifest-*.nd (glob)
721 721 00manifest.n
722 722 $ hg -R standard-clone debugnodemap --metadata
723 723 uid: * (glob)
724 724 tip-rev: 5005
725 725 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
726 726 data-length: 121088
727 727 data-unused: 0
728 728 data-unused: 0.000%
729 729
730 730
731 731 local clone
732 732 ------------
733 733
734 734 The persistent nodemap should exist after a streaming clone
735 735
736 736 $ hg clone -U test-repo local-clone
737 737 $ ls -1 local-clone/.hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
738 738 00changelog-*.nd (glob)
739 739 00changelog.n
740 740 00manifest-*.nd (glob)
741 741 00manifest.n
742 742 $ hg -R local-clone debugnodemap --metadata
743 743 uid: * (glob)
744 744 tip-rev: 5005
745 745 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
746 746 data-length: 121088
747 747 data-unused: 0
748 748 data-unused: 0.000%
749 749
750 750 Test various corruption case
751 751 ============================
752 752
753 753 Missing datafile
754 754 ----------------
755 755
756 756 Test behavior with a missing datafile
757 757
758 758 $ hg clone --quiet --pull test-repo corruption-test-repo
759 759 $ ls -1 corruption-test-repo/.hg/store/00changelog*
760 760 corruption-test-repo/.hg/store/00changelog-*.nd (glob)
761 761 corruption-test-repo/.hg/store/00changelog.d
762 762 corruption-test-repo/.hg/store/00changelog.i
763 763 corruption-test-repo/.hg/store/00changelog.n
764 764 $ rm corruption-test-repo/.hg/store/00changelog*.nd
765 765 $ hg log -R corruption-test-repo -r .
766 766 changeset: 5005:90d5d3ba2fc4
767 767 tag: tip
768 768 user: test
769 769 date: Thu Jan 01 00:00:00 1970 +0000
770 770 summary: a2
771 771
772 772 $ ls -1 corruption-test-repo/.hg/store/00changelog*
773 773 corruption-test-repo/.hg/store/00changelog.d
774 774 corruption-test-repo/.hg/store/00changelog.i
775 775 corruption-test-repo/.hg/store/00changelog.n
776 776
777 777 Truncated data file
778 778 -------------------
779 779
780 780 Test behavior with a too short datafile
781 781
782 782 rebuild the missing data
783 783 $ hg -R corruption-test-repo debugupdatecache
784 784 $ ls -1 corruption-test-repo/.hg/store/00changelog*
785 785 corruption-test-repo/.hg/store/00changelog-*.nd (glob)
786 786 corruption-test-repo/.hg/store/00changelog.d
787 787 corruption-test-repo/.hg/store/00changelog.i
788 788 corruption-test-repo/.hg/store/00changelog.n
789 789
790 790 truncate the file
791 791
792 792 $ datafilepath=`ls corruption-test-repo/.hg/store/00changelog*.nd`
793 793 $ f -s $datafilepath
794 794 corruption-test-repo/.hg/store/00changelog-*.nd: size=121088 (glob)
795 795 $ dd if=$datafilepath bs=1000 count=10 of=$datafilepath-tmp status=none
796 796 $ mv $datafilepath-tmp $datafilepath
797 797 $ f -s $datafilepath
798 798 corruption-test-repo/.hg/store/00changelog-*.nd: size=10000 (glob)
799 799
800 800 Check that Mercurial reaction to this event
801 801
802 802 $ hg -R corruption-test-repo log -r . --traceback
803 803 changeset: 5005:90d5d3ba2fc4
804 804 tag: tip
805 805 user: test
806 806 date: Thu Jan 01 00:00:00 1970 +0000
807 807 summary: a2
808 808
809 809
810 810
811 811 stream clone
812 812 ============
813 813
814 814 The persistent nodemap should exist after a streaming clone
815 815
816 816 Simple case
817 817 -----------
818 818
819 819 No race condition
820 820
821 821 $ hg clone -U --stream --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/test-repo stream-clone --debug | egrep '00(changelog|manifest)'
822 822 adding [s] 00manifest.n (70 bytes)
823 adding [s] 00manifest.d (452 KB) (no-zstd !)
824 adding [s] 00manifest.d (491 KB) (zstd !)
825 823 adding [s] 00manifest-*.nd (118 KB) (glob)
826 824 adding [s] 00changelog.n (70 bytes)
825 adding [s] 00changelog-*.nd (118 KB) (glob)
826 adding [s] 00manifest.d (452 KB) (no-zstd !)
827 adding [s] 00manifest.d (491 KB) (zstd !)
827 828 adding [s] 00changelog.d (360 KB) (no-zstd !)
828 829 adding [s] 00changelog.d (368 KB) (zstd !)
829 adding [s] 00changelog-*.nd (118 KB) (glob)
830 830 adding [s] 00manifest.i (313 KB)
831 831 adding [s] 00changelog.i (313 KB)
832 832 $ ls -1 stream-clone/.hg/store/ | egrep '00(changelog|manifest)(\.n|-.*\.nd)'
833 833 00changelog-*.nd (glob)
834 834 00changelog.n
835 835 00manifest-*.nd (glob)
836 836 00manifest.n
837 837 $ hg -R stream-clone debugnodemap --metadata
838 838 uid: * (glob)
839 839 tip-rev: 5005
840 840 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
841 841 data-length: 121088
842 842 data-unused: 0
843 843 data-unused: 0.000%
844 844
845 845 new data appened
846 846 -----------------
847 847
848 848 Other commit happening on the server during the stream clone
849 849
850 850 setup the step-by-step stream cloning
851 851
852 852 $ HG_TEST_STREAM_WALKED_FILE_1="$TESTTMP/sync_file_walked_1"
853 853 $ export HG_TEST_STREAM_WALKED_FILE_1
854 854 $ HG_TEST_STREAM_WALKED_FILE_2="$TESTTMP/sync_file_walked_2"
855 855 $ export HG_TEST_STREAM_WALKED_FILE_2
856 856 $ HG_TEST_STREAM_WALKED_FILE_3="$TESTTMP/sync_file_walked_3"
857 857 $ export HG_TEST_STREAM_WALKED_FILE_3
858 858 $ cat << EOF >> test-repo/.hg/hgrc
859 859 > [extensions]
860 860 > steps=$RUNTESTDIR/testlib/ext-stream-clone-steps.py
861 861 > EOF
862 862
863 863 Check and record file state beforehand
864 864
865 865 $ f --size test-repo/.hg/store/00changelog*
866 866 test-repo/.hg/store/00changelog-*.nd: size=121088 (glob)
867 867 test-repo/.hg/store/00changelog.d: size=376891 (zstd !)
868 868 test-repo/.hg/store/00changelog.d: size=368890 (no-zstd !)
869 869 test-repo/.hg/store/00changelog.i: size=320384
870 870 test-repo/.hg/store/00changelog.n: size=70
871 871 $ hg -R test-repo debugnodemap --metadata | tee server-metadata.txt
872 872 uid: * (glob)
873 873 tip-rev: 5005
874 874 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
875 875 data-length: 121088
876 876 data-unused: 0
877 877 data-unused: 0.000%
878 878
879 879 Prepare a commit
880 880
881 881 $ echo foo >> test-repo/foo
882 882 $ hg -R test-repo/ add test-repo/foo
883 883
884 884 Do a mix of clone and commit at the same time so that the file listed on disk differ at actual transfer time.
885 885
886 886 $ (hg clone -U --stream --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/test-repo stream-clone-race-1 --debug 2>> clone-output | egrep '00(changelog|manifest)' >> clone-output; touch $HG_TEST_STREAM_WALKED_FILE_3) &
887 887 $ $RUNTESTDIR/testlib/wait-on-file 10 $HG_TEST_STREAM_WALKED_FILE_1
888 888 $ hg -R test-repo/ commit -m foo
889 889 $ touch $HG_TEST_STREAM_WALKED_FILE_2
890 890 $ $RUNTESTDIR/testlib/wait-on-file 10 $HG_TEST_STREAM_WALKED_FILE_3
891 891 $ cat clone-output
892 remote: abort: unexpected error: [Errno 2] $ENOENT$: *'$TESTTMP/test-repo/.hg/store/00manifest-*.nd' (glob) (known-bad-output no-rust no-pure !)
893 abort: pull failed on remote (known-bad-output no-rust no-pure !)
894 892 adding [s] 00manifest.n (70 bytes)
895 adding [s] 00manifest.d (491 KB) (zstd !)
893 adding [s] 00manifest-*.nd (118 KB) (glob)
894 adding [s] 00changelog.n (70 bytes)
895 adding [s] 00changelog-*.nd (118 KB) (glob)
896 896 adding [s] 00manifest.d (452 KB) (no-zstd !)
897 remote: abort: $ENOENT$: '$TESTTMP/test-repo/.hg/store/00manifest-*.nd' (glob) (known-bad-output no-rust no-pure !)
898 adding [s] 00manifest-*.nd (118 KB) (glob) (rust !)
899 adding [s] 00changelog.n (70 bytes) (rust !)
900 adding [s] 00changelog.d (368 KB) (zstd rust !)
901 adding [s] 00changelog-*.nd (118 KB) (glob) (rust !)
902 adding [s] 00manifest.i (313 KB) (rust !)
903 adding [s] 00changelog.i (313 KB) (rust !)
904 adding [s] 00manifest-*.nd (118 KB) (glob) (pure !)
905 adding [s] 00changelog.n (70 bytes) (pure !)
897 adding [s] 00manifest.d (491 KB) (zstd !)
906 898 adding [s] 00changelog.d (360 KB) (no-zstd !)
907 adding [s] 00changelog-*.nd (118 KB) (glob) (pure !)
908 adding [s] 00manifest.i (313 KB) (pure !)
909 adding [s] 00changelog.i (313 KB) (pure !)
899 adding [s] 00changelog.d (368 KB) (zstd !)
900 adding [s] 00manifest.i (313 KB)
901 adding [s] 00changelog.i (313 KB)
910 902
911 903 Check the result state
912 904
913 905 $ f --size stream-clone-race-1/.hg/store/00changelog*
914 stream-clone-race-1/.hg/store/00changelog*: file not found (known-bad-output no-rust no-pure !)
915 stream-clone-race-1/.hg/store/00changelog-*.nd: size=121088 (glob) (rust !)
916 stream-clone-race-1/.hg/store/00changelog.d: size=376891 (zstd rust !)
917 stream-clone-race-1/.hg/store/00changelog.i: size=320384 (rust !)
918 stream-clone-race-1/.hg/store/00changelog.n: size=70 (rust !)
919 stream-clone-race-1/.hg/store/00changelog-*.nd: size=121088 (glob) (pure !)
920 stream-clone-race-1/.hg/store/00changelog.d: size=368890 (no-zstd pure !)
921 stream-clone-race-1/.hg/store/00changelog.i: size=320384 (pure !)
922 stream-clone-race-1/.hg/store/00changelog.n: size=70 (pure !)
906 stream-clone-race-1/.hg/store/00changelog-*.nd: size=121088 (glob)
907 stream-clone-race-1/.hg/store/00changelog.d: size=368890 (no-zstd !)
908 stream-clone-race-1/.hg/store/00changelog.d: size=376891 (zstd !)
909 stream-clone-race-1/.hg/store/00changelog.i: size=320384
910 stream-clone-race-1/.hg/store/00changelog.n: size=70
923 911
924 912 $ hg -R stream-clone-race-1 debugnodemap --metadata | tee client-metadata.txt
925 abort: repository stream-clone-race-1 not found (known-bad-output no-rust no-pure !)
926 uid: * (glob) (rust !)
927 tip-rev: 5005 (rust !)
928 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe (rust !)
929 data-length: 121088 (rust !)
930 data-unused: 0 (rust !)
931 data-unused: 0.000% (rust !)
932 uid: * (glob) (pure !)
933 tip-rev: 5005 (pure !)
934 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe (pure !)
935 data-length: 121088 (pure !)
936 data-unused: 0 (pure !)
937 data-unused: 0.000% (pure !)
913 uid: * (glob)
914 tip-rev: 5005
915 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
916 data-length: 121088
917 data-unused: 0
918 data-unused: 0.000%
938 919
939 920 We get a usable nodemap, so no rewrite would be needed and the metadata should be identical
940 921 (ie: the following diff should be empty)
941 922
923 This isn't the case for the `no-rust` `no-pure` implementation as it use a very minimal nodemap implementation that unconditionnaly rewrite the nodemap "all the time".
924
925 #if no-rust no-pure
942 926 $ diff -u server-metadata.txt client-metadata.txt
943 --- server-metadata.txt * (glob) (known-bad-output !)
944 +++ client-metadata.txt * (glob) (known-bad-output !)
945 @@ -1,4 +1,4 @@ (known-bad-output rust !)
946 @@ -1,4 +1,4 @@ (known-bad-output pure !)
947 @@ -1,6 +0,0 @@ (known-bad-output no-rust no-pure !)
948 -uid: * (glob) (known-bad-output !)
949 +uid: * (glob) (known-bad-output rust !)
950 tip-rev: 5005 (known-bad-output rust !)
951 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe (known-bad-output rust !)
952 data-length: 121088 (known-bad-output rust !)
953 +uid: * (glob) (known-bad-output pure !)
954 tip-rev: 5005 (known-bad-output pure !)
955 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe (known-bad-output pure !)
956 data-length: 121088 (known-bad-output pure !)
957 -tip-rev: 5005 (known-bad-output no-rust no-pure !)
958 -tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe (known-bad-output no-rust no-pure !)
959 -data-length: 121088 (known-bad-output no-rust no-pure !)
960 -data-unused: 0 (known-bad-output no-rust no-pure !)
961 -data-unused: 0.000% (known-bad-output no-rust no-pure !)
927 --- server-metadata.txt * (glob)
928 +++ client-metadata.txt * (glob)
929 @@ -1,4 +1,4 @@
930 -uid: * (glob)
931 +uid: * (glob)
932 tip-rev: 5005
933 tip-node: 90d5d3ba2fc47db50f712570487cb261a68c8ffe
934 data-length: 121088
962 935 [1]
936 #else
937 $ diff -u server-metadata.txt client-metadata.txt
938 #endif
939
963 940
964 941 Clean up after the test.
965 942
966 943 $ rm -f "$HG_TEST_STREAM_WALKED_FILE_1"
967 944 $ rm -f "$HG_TEST_STREAM_WALKED_FILE_2"
968 945 $ rm -f "$HG_TEST_STREAM_WALKED_FILE_3"
969 946
970 947 full regeneration
971 948 -----------------
972 949
973 950 A full nodemap is generated
974 951
975 952 (ideally this test would append enough data to make sure the nodemap data file
976 953 get changed, however to make thing simpler we will force the regeneration for
977 954 this test.
978 955
979 956 Check the initial state
980 957
981 958 $ f --size test-repo/.hg/store/00changelog*
982 959 test-repo/.hg/store/00changelog-*.nd: size=121344 (glob) (rust !)
983 960 test-repo/.hg/store/00changelog-*.nd: size=121344 (glob) (pure !)
984 961 test-repo/.hg/store/00changelog-*.nd: size=121152 (glob) (no-rust no-pure !)
985 962 test-repo/.hg/store/00changelog.d: size=376950 (zstd !)
986 963 test-repo/.hg/store/00changelog.d: size=368949 (no-zstd !)
987 964 test-repo/.hg/store/00changelog.i: size=320448
988 965 test-repo/.hg/store/00changelog.n: size=70
989 966 $ hg -R test-repo debugnodemap --metadata | tee server-metadata-2.txt
990 967 uid: * (glob)
991 968 tip-rev: 5006
992 969 tip-node: ed2ec1eef9aa2a0ec5057c51483bc148d03e810b
993 970 data-length: 121344 (rust !)
971 data-length: 121344 (pure !)
972 data-length: 121152 (no-rust no-pure !)
994 973 data-unused: 192 (rust !)
995 data-unused: 0.158% (rust !)
996 data-length: 121152 (no-rust no-pure !)
974 data-unused: 192 (pure !)
997 975 data-unused: 0 (no-rust no-pure !)
976 data-unused: 0.158% (rust !)
977 data-unused: 0.158% (pure !)
998 978 data-unused: 0.000% (no-rust no-pure !)
999 data-length: 121344 (pure !)
1000 data-unused: 192 (pure !)
1001 data-unused: 0.158% (pure !)
1002 979
1003 980 Performe the mix of clone and full refresh of the nodemap, so that the files
1004 981 (and filenames) are different between listing time and actual transfer time.
1005 982
1006 983 $ (hg clone -U --stream --config ui.ssh="\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/test-repo stream-clone-race-2 --debug 2>> clone-output-2 | egrep '00(changelog|manifest)' >> clone-output-2; touch $HG_TEST_STREAM_WALKED_FILE_3) &
1007 984 $ $RUNTESTDIR/testlib/wait-on-file 10 $HG_TEST_STREAM_WALKED_FILE_1
1008 985 $ rm test-repo/.hg/store/00changelog.n
1009 986 $ rm test-repo/.hg/store/00changelog-*.nd
1010 987 $ hg -R test-repo/ debugupdatecache
1011 988 $ touch $HG_TEST_STREAM_WALKED_FILE_2
1012 989 $ $RUNTESTDIR/testlib/wait-on-file 10 $HG_TEST_STREAM_WALKED_FILE_3
1013 990 $ cat clone-output-2
1014 remote: abort: unexpected error: [Errno 2] $ENOENT$: *'$TESTTMP/test-repo/.hg/store/00changelog-*.nd' (glob) (known-bad-output rust !)
1015 remote: abort: unexpected error: [Errno 2] $ENOENT$: *'$TESTTMP/test-repo/.hg/store/00changelog-*.nd' (glob) (known-bad-output pure !)
1016 remote: abort: unexpected error: [Errno 2] $ENOENT$: *'$TESTTMP/test-repo/.hg/store/00manifest-*.nd' (glob) (known-bad-output no-pure no-rust !)
1017 abort: pull failed on remote (known-bad-output !)
1018 991 adding [s] undo.backup.00manifest.n (70 bytes) (known-bad-output !)
1019 992 adding [s] undo.backup.00changelog.n (70 bytes) (known-bad-output !)
1020 993 adding [s] 00manifest.n (70 bytes)
994 adding [s] 00manifest-*.nd (118 KB) (glob)
995 adding [s] 00changelog.n (70 bytes)
996 adding [s] 00changelog-*.nd (118 KB) (glob)
1021 997 adding [s] 00manifest.d (492 KB) (zstd !)
1022 998 adding [s] 00manifest.d (452 KB) (no-zstd !)
1023 adding [s] 00manifest-*.nd (118 KB) (glob) (rust !)
1024 adding [s] 00manifest-*.nd (118 KB) (glob) (pure !)
1025 remote: abort: $ENOENT$: '$TESTTMP/test-repo/.hg/store/00changelog-*.nd' (glob) (known-bad-output rust !)
1026 remote: abort: $ENOENT$: '$TESTTMP/test-repo/.hg/store/00manifest-*.nd' (glob) (known-bad-output no-pure no-rust !)
1027 adding [s] 00changelog.n (70 bytes) (pure !)
1028 999 adding [s] 00changelog.d (360 KB) (no-zstd !)
1029 remote: abort: $ENOENT$: '$TESTTMP/test-repo/.hg/store/00changelog-*.nd' (glob) (known-bad-output pure !)
1000 adding [s] 00changelog.d (368 KB) (zstd !)
1001 adding [s] 00manifest.i (313 KB)
1002 adding [s] 00changelog.i (313 KB)
1030 1003
1031 1004 Check the result.
1032 1005
1033 1006 $ f --size stream-clone-race-2/.hg/store/00changelog*
1034 stream-clone-race-2/.hg/store/00changelog*: file not found (known-bad-output !)
1007 stream-clone-race-2/.hg/store/00changelog-*.nd: size=121344 (glob) (rust !)
1008 stream-clone-race-2/.hg/store/00changelog-*.nd: size=121344 (glob) (pure !)
1009 stream-clone-race-2/.hg/store/00changelog-*.nd: size=121152 (glob) (no-rust no-pure !)
1010 stream-clone-race-2/.hg/store/00changelog.d: size=376950 (zstd !)
1011 stream-clone-race-2/.hg/store/00changelog.d: size=368949 (no-zstd !)
1012 stream-clone-race-2/.hg/store/00changelog.i: size=320448
1013 stream-clone-race-2/.hg/store/00changelog.n: size=70
1035 1014
1036 1015 $ hg -R stream-clone-race-2 debugnodemap --metadata | tee client-metadata-2.txt
1037 abort: repository stream-clone-race-2 not found (known-bad-output !)
1016 uid: * (glob)
1017 tip-rev: 5006
1018 tip-node: ed2ec1eef9aa2a0ec5057c51483bc148d03e810b
1019 data-length: 121344 (rust !)
1020 data-unused: 192 (rust !)
1021 data-unused: 0.158% (rust !)
1022 data-length: 121152 (no-rust no-pure !)
1023 data-unused: 0 (no-rust no-pure !)
1024 data-unused: 0.000% (no-rust no-pure !)
1025 data-length: 121344 (pure !)
1026 data-unused: 192 (pure !)
1027 data-unused: 0.158% (pure !)
1038 1028
1039 1029 We get a usable nodemap, so no rewrite would be needed and the metadata should be identical
1040 1030 (ie: the following diff should be empty)
1041 1031
1032 This isn't the case for the `no-rust` `no-pure` implementation as it use a very minimal nodemap implementation that unconditionnaly rewrite the nodemap "all the time".
1033
1034 #if no-rust no-pure
1042 1035 $ diff -u server-metadata-2.txt client-metadata-2.txt
1043 --- server-metadata-2.txt * (glob) (known-bad-output !)
1044 +++ client-metadata-2.txt * (glob) (known-bad-output !)
1045 @@ -1,6 +0,0 @@ (known-bad-output !)
1046 -uid: * (glob) (known-bad-output !)
1047 -tip-rev: 5006 (known-bad-output !)
1048 -tip-node: ed2ec1eef9aa2a0ec5057c51483bc148d03e810b (known-bad-output !)
1049 -data-length: 121344 (known-bad-output rust !)
1050 -data-unused: 192 (known-bad-output rust !)
1051 -data-unused: 0.158% (known-bad-output rust !)
1052 -data-length: 121344 (known-bad-output pure !)
1053 -data-unused: 192 (known-bad-output pure !)
1054 -data-unused: 0.158% (known-bad-output pure !)
1055 -data-length: 121152 (known-bad-output no-rust no-pure !)
1056 -data-unused: 0 (known-bad-output no-rust no-pure !)
1057 -data-unused: 0.000% (known-bad-output no-rust no-pure !)
1036 --- server-metadata-2.txt * (glob)
1037 +++ client-metadata-2.txt * (glob)
1038 @@ -1,4 +1,4 @@
1039 -uid: * (glob)
1040 +uid: * (glob)
1041 tip-rev: 5006
1042 tip-node: ed2ec1eef9aa2a0ec5057c51483bc148d03e810b
1043 data-length: 121152
1058 1044 [1]
1045 #else
1046 $ diff -u server-metadata-2.txt client-metadata-2.txt
1047 #endif
1059 1048
1060 1049 Clean up after the test
1061 1050
1062 1051 $ rm -f $HG_TEST_STREAM_WALKED_FILE_1
1063 1052 $ rm -f $HG_TEST_STREAM_WALKED_FILE_2
1064 1053 $ rm -f $HG_TEST_STREAM_WALKED_FILE_3
1065 1054
General Comments 0
You need to be logged in to leave comments. Login now