##// END OF EJS Templates
store: introduce _matchtrackedpath() and use it to filter store files...
Pulkit Goyal -
r40529:9aeb9e2d default
parent child Browse files
Show More
@@ -1,591 +1,609
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@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 hashlib
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from . import (
17 17 error,
18 18 node,
19 19 policy,
20 20 pycompat,
21 21 util,
22 22 vfs as vfsmod,
23 23 )
24 24
25 25 parsers = policy.importmod(r'parsers')
26 26
27 def _matchtrackedpath(path, matcher):
28 """parses a fncache entry and returns whether the entry is tracking a path
29 matched by matcher or not.
30
31 If matcher is None, returns True"""
32
33 if matcher is None:
34 return True
35 path = decodedir(path)
36 if path.startswith('data/'):
37 return matcher(path[len('data/'):-len('.i')])
38 elif path.startswith('meta/'):
39 return matcher.visitdir(path[len('meta/'):-len('/00manifest.i')] or '.')
40
27 41 # This avoids a collision between a file named foo and a dir named
28 42 # foo.i or foo.d
29 43 def _encodedir(path):
30 44 '''
31 45 >>> _encodedir(b'data/foo.i')
32 46 'data/foo.i'
33 47 >>> _encodedir(b'data/foo.i/bla.i')
34 48 'data/foo.i.hg/bla.i'
35 49 >>> _encodedir(b'data/foo.i.hg/bla.i')
36 50 'data/foo.i.hg.hg/bla.i'
37 51 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
38 52 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
39 53 '''
40 54 return (path
41 55 .replace(".hg/", ".hg.hg/")
42 56 .replace(".i/", ".i.hg/")
43 57 .replace(".d/", ".d.hg/"))
44 58
45 59 encodedir = getattr(parsers, 'encodedir', _encodedir)
46 60
47 61 def decodedir(path):
48 62 '''
49 63 >>> decodedir(b'data/foo.i')
50 64 'data/foo.i'
51 65 >>> decodedir(b'data/foo.i.hg/bla.i')
52 66 'data/foo.i/bla.i'
53 67 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
54 68 'data/foo.i.hg/bla.i'
55 69 '''
56 70 if ".hg/" not in path:
57 71 return path
58 72 return (path
59 73 .replace(".d.hg/", ".d/")
60 74 .replace(".i.hg/", ".i/")
61 75 .replace(".hg.hg/", ".hg/"))
62 76
63 77 def _reserved():
64 78 ''' characters that are problematic for filesystems
65 79
66 80 * ascii escapes (0..31)
67 81 * ascii hi (126..255)
68 82 * windows specials
69 83
70 84 these characters will be escaped by encodefunctions
71 85 '''
72 86 winreserved = [ord(x) for x in u'\\:*?"<>|']
73 87 for x in range(32):
74 88 yield x
75 89 for x in range(126, 256):
76 90 yield x
77 91 for x in winreserved:
78 92 yield x
79 93
80 94 def _buildencodefun():
81 95 '''
82 96 >>> enc, dec = _buildencodefun()
83 97
84 98 >>> enc(b'nothing/special.txt')
85 99 'nothing/special.txt'
86 100 >>> dec(b'nothing/special.txt')
87 101 'nothing/special.txt'
88 102
89 103 >>> enc(b'HELLO')
90 104 '_h_e_l_l_o'
91 105 >>> dec(b'_h_e_l_l_o')
92 106 'HELLO'
93 107
94 108 >>> enc(b'hello:world?')
95 109 'hello~3aworld~3f'
96 110 >>> dec(b'hello~3aworld~3f')
97 111 'hello:world?'
98 112
99 113 >>> enc(b'the\\x07quick\\xADshot')
100 114 'the~07quick~adshot'
101 115 >>> dec(b'the~07quick~adshot')
102 116 'the\\x07quick\\xadshot'
103 117 '''
104 118 e = '_'
105 119 xchr = pycompat.bytechr
106 120 asciistr = list(map(xchr, range(127)))
107 121 capitals = list(range(ord("A"), ord("Z") + 1))
108 122
109 123 cmap = dict((x, x) for x in asciistr)
110 124 for x in _reserved():
111 125 cmap[xchr(x)] = "~%02x" % x
112 126 for x in capitals + [ord(e)]:
113 127 cmap[xchr(x)] = e + xchr(x).lower()
114 128
115 129 dmap = {}
116 130 for k, v in cmap.iteritems():
117 131 dmap[v] = k
118 132 def decode(s):
119 133 i = 0
120 134 while i < len(s):
121 135 for l in pycompat.xrange(1, 4):
122 136 try:
123 137 yield dmap[s[i:i + l]]
124 138 i += l
125 139 break
126 140 except KeyError:
127 141 pass
128 142 else:
129 143 raise KeyError
130 144 return (lambda s: ''.join([cmap[s[c:c + 1]]
131 145 for c in pycompat.xrange(len(s))]),
132 146 lambda s: ''.join(list(decode(s))))
133 147
134 148 _encodefname, _decodefname = _buildencodefun()
135 149
136 150 def encodefilename(s):
137 151 '''
138 152 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
139 153 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
140 154 '''
141 155 return _encodefname(encodedir(s))
142 156
143 157 def decodefilename(s):
144 158 '''
145 159 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
146 160 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
147 161 '''
148 162 return decodedir(_decodefname(s))
149 163
150 164 def _buildlowerencodefun():
151 165 '''
152 166 >>> f = _buildlowerencodefun()
153 167 >>> f(b'nothing/special.txt')
154 168 'nothing/special.txt'
155 169 >>> f(b'HELLO')
156 170 'hello'
157 171 >>> f(b'hello:world?')
158 172 'hello~3aworld~3f'
159 173 >>> f(b'the\\x07quick\\xADshot')
160 174 'the~07quick~adshot'
161 175 '''
162 176 xchr = pycompat.bytechr
163 177 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
164 178 for x in _reserved():
165 179 cmap[xchr(x)] = "~%02x" % x
166 180 for x in range(ord("A"), ord("Z") + 1):
167 181 cmap[xchr(x)] = xchr(x).lower()
168 182 def lowerencode(s):
169 183 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
170 184 return lowerencode
171 185
172 186 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
173 187
174 188 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
175 189 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
176 190 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
177 191 def _auxencode(path, dotencode):
178 192 '''
179 193 Encodes filenames containing names reserved by Windows or which end in
180 194 period or space. Does not touch other single reserved characters c.
181 195 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
182 196 Additionally encodes space or period at the beginning, if dotencode is
183 197 True. Parameter path is assumed to be all lowercase.
184 198 A segment only needs encoding if a reserved name appears as a
185 199 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
186 200 doesn't need encoding.
187 201
188 202 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
189 203 >>> _auxencode(s.split(b'/'), True)
190 204 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
191 205 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
192 206 >>> _auxencode(s.split(b'/'), False)
193 207 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
194 208 >>> _auxencode([b'foo. '], True)
195 209 ['foo.~20']
196 210 >>> _auxencode([b' .foo'], True)
197 211 ['~20.foo']
198 212 '''
199 213 for i, n in enumerate(path):
200 214 if not n:
201 215 continue
202 216 if dotencode and n[0] in '. ':
203 217 n = "~%02x" % ord(n[0:1]) + n[1:]
204 218 path[i] = n
205 219 else:
206 220 l = n.find('.')
207 221 if l == -1:
208 222 l = len(n)
209 223 if ((l == 3 and n[:3] in _winres3) or
210 224 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
211 225 and n[:3] in _winres4)):
212 226 # encode third letter ('aux' -> 'au~78')
213 227 ec = "~%02x" % ord(n[2:3])
214 228 n = n[0:2] + ec + n[3:]
215 229 path[i] = n
216 230 if n[-1] in '. ':
217 231 # encode last period or space ('foo...' -> 'foo..~2e')
218 232 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
219 233 return path
220 234
221 235 _maxstorepathlen = 120
222 236 _dirprefixlen = 8
223 237 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
224 238
225 239 def _hashencode(path, dotencode):
226 240 digest = node.hex(hashlib.sha1(path).digest())
227 241 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
228 242 parts = _auxencode(le, dotencode)
229 243 basename = parts[-1]
230 244 _root, ext = os.path.splitext(basename)
231 245 sdirs = []
232 246 sdirslen = 0
233 247 for p in parts[:-1]:
234 248 d = p[:_dirprefixlen]
235 249 if d[-1] in '. ':
236 250 # Windows can't access dirs ending in period or space
237 251 d = d[:-1] + '_'
238 252 if sdirslen == 0:
239 253 t = len(d)
240 254 else:
241 255 t = sdirslen + 1 + len(d)
242 256 if t > _maxshortdirslen:
243 257 break
244 258 sdirs.append(d)
245 259 sdirslen = t
246 260 dirs = '/'.join(sdirs)
247 261 if len(dirs) > 0:
248 262 dirs += '/'
249 263 res = 'dh/' + dirs + digest + ext
250 264 spaceleft = _maxstorepathlen - len(res)
251 265 if spaceleft > 0:
252 266 filler = basename[:spaceleft]
253 267 res = 'dh/' + dirs + filler + digest + ext
254 268 return res
255 269
256 270 def _hybridencode(path, dotencode):
257 271 '''encodes path with a length limit
258 272
259 273 Encodes all paths that begin with 'data/', according to the following.
260 274
261 275 Default encoding (reversible):
262 276
263 277 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
264 278 characters are encoded as '~xx', where xx is the two digit hex code
265 279 of the character (see encodefilename).
266 280 Relevant path components consisting of Windows reserved filenames are
267 281 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
268 282
269 283 Hashed encoding (not reversible):
270 284
271 285 If the default-encoded path is longer than _maxstorepathlen, a
272 286 non-reversible hybrid hashing of the path is done instead.
273 287 This encoding uses up to _dirprefixlen characters of all directory
274 288 levels of the lowerencoded path, but not more levels than can fit into
275 289 _maxshortdirslen.
276 290 Then follows the filler followed by the sha digest of the full path.
277 291 The filler is the beginning of the basename of the lowerencoded path
278 292 (the basename is everything after the last path separator). The filler
279 293 is as long as possible, filling in characters from the basename until
280 294 the encoded path has _maxstorepathlen characters (or all chars of the
281 295 basename have been taken).
282 296 The extension (e.g. '.i' or '.d') is preserved.
283 297
284 298 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
285 299 encoding was used.
286 300 '''
287 301 path = encodedir(path)
288 302 ef = _encodefname(path).split('/')
289 303 res = '/'.join(_auxencode(ef, dotencode))
290 304 if len(res) > _maxstorepathlen:
291 305 res = _hashencode(path, dotencode)
292 306 return res
293 307
294 308 def _pathencode(path):
295 309 de = encodedir(path)
296 310 if len(path) > _maxstorepathlen:
297 311 return _hashencode(de, True)
298 312 ef = _encodefname(de).split('/')
299 313 res = '/'.join(_auxencode(ef, True))
300 314 if len(res) > _maxstorepathlen:
301 315 return _hashencode(de, True)
302 316 return res
303 317
304 318 _pathencode = getattr(parsers, 'pathencode', _pathencode)
305 319
306 320 def _plainhybridencode(f):
307 321 return _hybridencode(f, False)
308 322
309 323 def _calcmode(vfs):
310 324 try:
311 325 # files in .hg/ will be created using this mode
312 326 mode = vfs.stat().st_mode
313 327 # avoid some useless chmods
314 328 if (0o777 & ~util.umask) == (0o777 & mode):
315 329 mode = None
316 330 except OSError:
317 331 mode = None
318 332 return mode
319 333
320 334 _data = ('narrowspec data meta 00manifest.d 00manifest.i'
321 335 ' 00changelog.d 00changelog.i phaseroots obsstore')
322 336
323 337 def isrevlog(f, kind, st):
324 338 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
325 339
326 340 class basicstore(object):
327 341 '''base class for local repository stores'''
328 342 def __init__(self, path, vfstype):
329 343 vfs = vfstype(path)
330 344 self.path = vfs.base
331 345 self.createmode = _calcmode(vfs)
332 346 vfs.createmode = self.createmode
333 347 self.rawvfs = vfs
334 348 self.vfs = vfsmod.filtervfs(vfs, encodedir)
335 349 self.opener = self.vfs
336 350
337 351 def join(self, f):
338 352 return self.path + '/' + encodedir(f)
339 353
340 354 def _walk(self, relpath, recurse, filefilter=isrevlog):
341 355 '''yields (unencoded, encoded, size)'''
342 356 path = self.path
343 357 if relpath:
344 358 path += '/' + relpath
345 359 striplen = len(self.path) + 1
346 360 l = []
347 361 if self.rawvfs.isdir(path):
348 362 visit = [path]
349 363 readdir = self.rawvfs.readdir
350 364 while visit:
351 365 p = visit.pop()
352 366 for f, kind, st in readdir(p, stat=True):
353 367 fp = p + '/' + f
354 368 if filefilter(f, kind, st):
355 369 n = util.pconvert(fp[striplen:])
356 370 l.append((decodedir(n), n, st.st_size))
357 371 elif kind == stat.S_IFDIR and recurse:
358 372 visit.append(fp)
359 373 l.sort()
360 374 return l
361 375
362 376 def datafiles(self, matcher=None):
363 377 return self._walk('data', True) + self._walk('meta', True)
364 378
365 379 def topfiles(self):
366 380 # yield manifest before changelog
367 381 return reversed(self._walk('', False))
368 382
369 383 def walk(self, matcher=None):
370 384 '''yields (unencoded, encoded, size)
371 385
372 386 if a matcher is passed, storage files of only those tracked paths
373 387 are passed with matches the matcher
374 388 '''
375 389 # yield data files first
376 390 for x in self.datafiles(matcher):
377 391 yield x
378 392 for x in self.topfiles():
379 393 yield x
380 394
381 395 def copylist(self):
382 396 return ['requires'] + _data.split()
383 397
384 398 def write(self, tr):
385 399 pass
386 400
387 401 def invalidatecaches(self):
388 402 pass
389 403
390 404 def markremoved(self, fn):
391 405 pass
392 406
393 407 def __contains__(self, path):
394 408 '''Checks if the store contains path'''
395 409 path = "/".join(("data", path))
396 410 # file?
397 411 if self.vfs.exists(path + ".i"):
398 412 return True
399 413 # dir?
400 414 if not path.endswith("/"):
401 415 path = path + "/"
402 416 return self.vfs.exists(path)
403 417
404 418 class encodedstore(basicstore):
405 419 def __init__(self, path, vfstype):
406 420 vfs = vfstype(path + '/store')
407 421 self.path = vfs.base
408 422 self.createmode = _calcmode(vfs)
409 423 vfs.createmode = self.createmode
410 424 self.rawvfs = vfs
411 425 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
412 426 self.opener = self.vfs
413 427
414 428 def datafiles(self, matcher=None):
415 429 for a, b, size in super(encodedstore, self).datafiles():
430 if not _matchtrackedpath(a, matcher):
431 continue
416 432 try:
417 433 a = decodefilename(a)
418 434 except KeyError:
419 435 a = None
420 436 yield a, b, size
421 437
422 438 def join(self, f):
423 439 return self.path + '/' + encodefilename(f)
424 440
425 441 def copylist(self):
426 442 return (['requires', '00changelog.i'] +
427 443 ['store/' + f for f in _data.split()])
428 444
429 445 class fncache(object):
430 446 # the filename used to be partially encoded
431 447 # hence the encodedir/decodedir dance
432 448 def __init__(self, vfs):
433 449 self.vfs = vfs
434 450 self.entries = None
435 451 self._dirty = False
436 452
437 453 def _load(self):
438 454 '''fill the entries from the fncache file'''
439 455 self._dirty = False
440 456 try:
441 457 fp = self.vfs('fncache', mode='rb')
442 458 except IOError:
443 459 # skip nonexistent file
444 460 self.entries = set()
445 461 return
446 462 self.entries = set(decodedir(fp.read()).splitlines())
447 463 if '' in self.entries:
448 464 fp.seek(0)
449 465 for n, line in enumerate(util.iterfile(fp)):
450 466 if not line.rstrip('\n'):
451 467 t = _('invalid entry in fncache, line %d') % (n + 1)
452 468 raise error.Abort(t)
453 469 fp.close()
454 470
455 471 def write(self, tr):
456 472 if self._dirty:
457 473 assert self.entries is not None
458 474 tr.addbackup('fncache')
459 475 fp = self.vfs('fncache', mode='wb', atomictemp=True)
460 476 if self.entries:
461 477 fp.write(encodedir('\n'.join(self.entries) + '\n'))
462 478 fp.close()
463 479 self._dirty = False
464 480
465 481 def add(self, fn):
466 482 if self.entries is None:
467 483 self._load()
468 484 if fn not in self.entries:
469 485 self._dirty = True
470 486 self.entries.add(fn)
471 487
472 488 def remove(self, fn):
473 489 if self.entries is None:
474 490 self._load()
475 491 try:
476 492 self.entries.remove(fn)
477 493 self._dirty = True
478 494 except KeyError:
479 495 pass
480 496
481 497 def __contains__(self, fn):
482 498 if self.entries is None:
483 499 self._load()
484 500 return fn in self.entries
485 501
486 502 def __iter__(self):
487 503 if self.entries is None:
488 504 self._load()
489 505 return iter(self.entries)
490 506
491 507 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
492 508 def __init__(self, vfs, fnc, encode):
493 509 vfsmod.proxyvfs.__init__(self, vfs)
494 510 self.fncache = fnc
495 511 self.encode = encode
496 512
497 513 def __call__(self, path, mode='r', *args, **kw):
498 514 encoded = self.encode(path)
499 515 if mode not in ('r', 'rb') and (path.startswith('data/') or
500 516 path.startswith('meta/')):
501 517 # do not trigger a fncache load when adding a file that already is
502 518 # known to exist.
503 519 notload = self.fncache.entries is None and self.vfs.exists(encoded)
504 520 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
505 521 # when appending to an existing file, if the file has size zero,
506 522 # it should be considered as missing. Such zero-size files are
507 523 # the result of truncation when a transaction is aborted.
508 524 notload = False
509 525 if not notload:
510 526 self.fncache.add(path)
511 527 return self.vfs(encoded, mode, *args, **kw)
512 528
513 529 def join(self, path):
514 530 if path:
515 531 return self.vfs.join(self.encode(path))
516 532 else:
517 533 return self.vfs.join(path)
518 534
519 535 class fncachestore(basicstore):
520 536 def __init__(self, path, vfstype, dotencode):
521 537 if dotencode:
522 538 encode = _pathencode
523 539 else:
524 540 encode = _plainhybridencode
525 541 self.encode = encode
526 542 vfs = vfstype(path + '/store')
527 543 self.path = vfs.base
528 544 self.pathsep = self.path + '/'
529 545 self.createmode = _calcmode(vfs)
530 546 vfs.createmode = self.createmode
531 547 self.rawvfs = vfs
532 548 fnc = fncache(vfs)
533 549 self.fncache = fnc
534 550 self.vfs = _fncachevfs(vfs, fnc, encode)
535 551 self.opener = self.vfs
536 552
537 553 def join(self, f):
538 554 return self.pathsep + self.encode(f)
539 555
540 556 def getsize(self, path):
541 557 return self.rawvfs.stat(path).st_size
542 558
543 559 def datafiles(self, matcher=None):
544 560 for f in sorted(self.fncache):
561 if not _matchtrackedpath(f, matcher):
562 continue
545 563 ef = self.encode(f)
546 564 try:
547 565 yield f, ef, self.getsize(ef)
548 566 except OSError as err:
549 567 if err.errno != errno.ENOENT:
550 568 raise
551 569
552 570 def copylist(self):
553 571 d = ('narrowspec data meta dh fncache phaseroots obsstore'
554 572 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
555 573 return (['requires', '00changelog.i'] +
556 574 ['store/' + f for f in d.split()])
557 575
558 576 def write(self, tr):
559 577 self.fncache.write(tr)
560 578
561 579 def invalidatecaches(self):
562 580 self.fncache.entries = None
563 581
564 582 def markremoved(self, fn):
565 583 self.fncache.remove(fn)
566 584
567 585 def _exists(self, f):
568 586 ef = self.encode(f)
569 587 try:
570 588 self.getsize(ef)
571 589 return True
572 590 except OSError as err:
573 591 if err.errno != errno.ENOENT:
574 592 raise
575 593 # nonexistent entry
576 594 return False
577 595
578 596 def __contains__(self, path):
579 597 '''Checks if the store contains path'''
580 598 path = "/".join(("data", path))
581 599 # check for files (exact match)
582 600 e = path + '.i'
583 601 if e in self.fncache and self._exists(e):
584 602 return True
585 603 # now check for directories (prefix match)
586 604 if not path.endswith('/'):
587 605 path += '/'
588 606 for e in self.fncache:
589 607 if e.startswith(path) and self._exists(e):
590 608 return True
591 609 return False
@@ -1,663 +1,659
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 . import (
16 16 branchmap,
17 17 cacheutil,
18 18 error,
19 19 narrowspec,
20 20 phases,
21 21 pycompat,
22 22 repository,
23 23 store,
24 24 util,
25 25 )
26 26
27 27 def canperformstreamclone(pullop, bundle2=False):
28 28 """Whether it is possible to perform a streaming clone as part of pull.
29 29
30 30 ``bundle2`` will cause the function to consider stream clone through
31 31 bundle2 and only through bundle2.
32 32
33 33 Returns a tuple of (supported, requirements). ``supported`` is True if
34 34 streaming clone is supported and False otherwise. ``requirements`` is
35 35 a set of repo requirements from the remote, or ``None`` if stream clone
36 36 isn't supported.
37 37 """
38 38 repo = pullop.repo
39 39 remote = pullop.remote
40 40
41 41 bundle2supported = False
42 42 if pullop.canusebundle2:
43 43 if 'v2' in pullop.remotebundle2caps.get('stream', []):
44 44 bundle2supported = True
45 45 # else
46 46 # Server doesn't support bundle2 stream clone or doesn't support
47 47 # the versions we support. Fall back and possibly allow legacy.
48 48
49 49 # Ensures legacy code path uses available bundle2.
50 50 if bundle2supported and not bundle2:
51 51 return False, None
52 52 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
53 53 elif bundle2 and not bundle2supported:
54 54 return False, None
55 55
56 56 # Streaming clone only works on empty repositories.
57 57 if len(repo):
58 58 return False, None
59 59
60 60 # Streaming clone only works if all data is being requested.
61 61 if pullop.heads:
62 62 return False, None
63 63
64 64 streamrequested = pullop.streamclonerequested
65 65
66 66 # If we don't have a preference, let the server decide for us. This
67 67 # likely only comes into play in LANs.
68 68 if streamrequested is None:
69 69 # The server can advertise whether to prefer streaming clone.
70 70 streamrequested = remote.capable('stream-preferred')
71 71
72 72 if not streamrequested:
73 73 return False, None
74 74
75 75 # In order for stream clone to work, the client has to support all the
76 76 # requirements advertised by the server.
77 77 #
78 78 # The server advertises its requirements via the "stream" and "streamreqs"
79 79 # capability. "stream" (a value-less capability) is advertised if and only
80 80 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
81 81 # is advertised and contains a comma-delimited list of requirements.
82 82 requirements = set()
83 83 if remote.capable('stream'):
84 84 requirements.add('revlogv1')
85 85 else:
86 86 streamreqs = remote.capable('streamreqs')
87 87 # This is weird and shouldn't happen with modern servers.
88 88 if not streamreqs:
89 89 pullop.repo.ui.warn(_(
90 90 'warning: stream clone requested but server has them '
91 91 'disabled\n'))
92 92 return False, None
93 93
94 94 streamreqs = set(streamreqs.split(','))
95 95 # Server requires something we don't support. Bail.
96 96 missingreqs = streamreqs - repo.supportedformats
97 97 if missingreqs:
98 98 pullop.repo.ui.warn(_(
99 99 'warning: stream clone requested but client is missing '
100 100 'requirements: %s\n') % ', '.join(sorted(missingreqs)))
101 101 pullop.repo.ui.warn(
102 102 _('(see https://www.mercurial-scm.org/wiki/MissingRequirement '
103 103 'for more information)\n'))
104 104 return False, None
105 105 requirements = streamreqs
106 106
107 107 return True, requirements
108 108
109 109 def maybeperformlegacystreamclone(pullop):
110 110 """Possibly perform a legacy stream clone operation.
111 111
112 112 Legacy stream clones are performed as part of pull but before all other
113 113 operations.
114 114
115 115 A legacy stream clone will not be performed if a bundle2 stream clone is
116 116 supported.
117 117 """
118 118 from . import localrepo
119 119
120 120 supported, requirements = canperformstreamclone(pullop)
121 121
122 122 if not supported:
123 123 return
124 124
125 125 repo = pullop.repo
126 126 remote = pullop.remote
127 127
128 128 # Save remote branchmap. We will use it later to speed up branchcache
129 129 # creation.
130 130 rbranchmap = None
131 131 if remote.capable('branchmap'):
132 132 with remote.commandexecutor() as e:
133 133 rbranchmap = e.callcommand('branchmap', {}).result()
134 134
135 135 repo.ui.status(_('streaming all changes\n'))
136 136
137 137 with remote.commandexecutor() as e:
138 138 fp = e.callcommand('stream_out', {}).result()
139 139
140 140 # TODO strictly speaking, this code should all be inside the context
141 141 # manager because the context manager is supposed to ensure all wire state
142 142 # is flushed when exiting. But the legacy peers don't do this, so it
143 143 # doesn't matter.
144 144 l = fp.readline()
145 145 try:
146 146 resp = int(l)
147 147 except ValueError:
148 148 raise error.ResponseError(
149 149 _('unexpected response from remote server:'), l)
150 150 if resp == 1:
151 151 raise error.Abort(_('operation forbidden by server'))
152 152 elif resp == 2:
153 153 raise error.Abort(_('locking the remote repository failed'))
154 154 elif resp != 0:
155 155 raise error.Abort(_('the server sent an unknown error code'))
156 156
157 157 l = fp.readline()
158 158 try:
159 159 filecount, bytecount = map(int, l.split(' ', 1))
160 160 except (ValueError, TypeError):
161 161 raise error.ResponseError(
162 162 _('unexpected response from remote server:'), l)
163 163
164 164 with repo.lock():
165 165 consumev1(repo, fp, filecount, bytecount)
166 166
167 167 # new requirements = old non-format requirements +
168 168 # new format-related remote requirements
169 169 # requirements from the streamed-in repository
170 170 repo.requirements = requirements | (
171 171 repo.requirements - repo.supportedformats)
172 172 repo.svfs.options = localrepo.resolvestorevfsoptions(
173 173 repo.ui, repo.requirements, repo.features)
174 174 repo._writerequirements()
175 175
176 176 if rbranchmap:
177 177 branchmap.replacecache(repo, rbranchmap)
178 178
179 179 repo.invalidate()
180 180
181 181 def allowservergeneration(repo):
182 182 """Whether streaming clones are allowed from the server."""
183 183 if repository.REPO_FEATURE_STREAM_CLONE not in repo.features:
184 184 return False
185 185
186 186 if not repo.ui.configbool('server', 'uncompressed', untrusted=True):
187 187 return False
188 188
189 189 # The way stream clone works makes it impossible to hide secret changesets.
190 190 # So don't allow this by default.
191 191 secret = phases.hassecret(repo)
192 192 if secret:
193 193 return repo.ui.configbool('server', 'uncompressedallowsecret')
194 194
195 195 return True
196 196
197 197 # This is it's own function so extensions can override it.
198 198 def _walkstreamfiles(repo, matcher=None):
199 199 return repo.store.walk(matcher)
200 200
201 201 def generatev1(repo):
202 202 """Emit content for version 1 of a streaming clone.
203 203
204 204 This returns a 3-tuple of (file count, byte size, data iterator).
205 205
206 206 The data iterator consists of N entries for each file being transferred.
207 207 Each file entry starts as a line with the file name and integer size
208 208 delimited by a null byte.
209 209
210 210 The raw file data follows. Following the raw file data is the next file
211 211 entry, or EOF.
212 212
213 213 When used on the wire protocol, an additional line indicating protocol
214 214 success will be prepended to the stream. This function is not responsible
215 215 for adding it.
216 216
217 217 This function will obtain a repository lock to ensure a consistent view of
218 218 the store is captured. It therefore may raise LockError.
219 219 """
220 220 entries = []
221 221 total_bytes = 0
222 222 # Get consistent snapshot of repo, lock during scan.
223 223 with repo.lock():
224 224 repo.ui.debug('scanning\n')
225 225 for name, ename, size in _walkstreamfiles(repo):
226 226 if size:
227 227 entries.append((name, size))
228 228 total_bytes += size
229 229
230 230 repo.ui.debug('%d files, %d bytes to transfer\n' %
231 231 (len(entries), total_bytes))
232 232
233 233 svfs = repo.svfs
234 234 debugflag = repo.ui.debugflag
235 235
236 236 def emitrevlogdata():
237 237 for name, size in entries:
238 238 if debugflag:
239 239 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
240 240 # partially encode name over the wire for backwards compat
241 241 yield '%s\0%d\n' % (store.encodedir(name), size)
242 242 # auditing at this stage is both pointless (paths are already
243 243 # trusted by the local repo) and expensive
244 244 with svfs(name, 'rb', auditpath=False) as fp:
245 245 if size <= 65536:
246 246 yield fp.read(size)
247 247 else:
248 248 for chunk in util.filechunkiter(fp, limit=size):
249 249 yield chunk
250 250
251 251 return len(entries), total_bytes, emitrevlogdata()
252 252
253 253 def generatev1wireproto(repo):
254 254 """Emit content for version 1 of streaming clone suitable for the wire.
255 255
256 256 This is the data output from ``generatev1()`` with 2 header lines. The
257 257 first line indicates overall success. The 2nd contains the file count and
258 258 byte size of payload.
259 259
260 260 The success line contains "0" for success, "1" for stream generation not
261 261 allowed, and "2" for error locking the repository (possibly indicating
262 262 a permissions error for the server process).
263 263 """
264 264 if not allowservergeneration(repo):
265 265 yield '1\n'
266 266 return
267 267
268 268 try:
269 269 filecount, bytecount, it = generatev1(repo)
270 270 except error.LockError:
271 271 yield '2\n'
272 272 return
273 273
274 274 # Indicates successful response.
275 275 yield '0\n'
276 276 yield '%d %d\n' % (filecount, bytecount)
277 277 for chunk in it:
278 278 yield chunk
279 279
280 280 def generatebundlev1(repo, compression='UN'):
281 281 """Emit content for version 1 of a stream clone bundle.
282 282
283 283 The first 4 bytes of the output ("HGS1") denote this as stream clone
284 284 bundle version 1.
285 285
286 286 The next 2 bytes indicate the compression type. Only "UN" is currently
287 287 supported.
288 288
289 289 The next 16 bytes are two 64-bit big endian unsigned integers indicating
290 290 file count and byte count, respectively.
291 291
292 292 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
293 293 of the requirements string, including a trailing \0. The following N bytes
294 294 are the requirements string, which is ASCII containing a comma-delimited
295 295 list of repo requirements that are needed to support the data.
296 296
297 297 The remaining content is the output of ``generatev1()`` (which may be
298 298 compressed in the future).
299 299
300 300 Returns a tuple of (requirements, data generator).
301 301 """
302 302 if compression != 'UN':
303 303 raise ValueError('we do not support the compression argument yet')
304 304
305 305 requirements = repo.requirements & repo.supportedformats
306 306 requires = ','.join(sorted(requirements))
307 307
308 308 def gen():
309 309 yield 'HGS1'
310 310 yield compression
311 311
312 312 filecount, bytecount, it = generatev1(repo)
313 313 repo.ui.status(_('writing %d bytes for %d files\n') %
314 314 (bytecount, filecount))
315 315
316 316 yield struct.pack('>QQ', filecount, bytecount)
317 317 yield struct.pack('>H', len(requires) + 1)
318 318 yield requires + '\0'
319 319
320 320 # This is where we'll add compression in the future.
321 321 assert compression == 'UN'
322 322
323 323 progress = repo.ui.makeprogress(_('bundle'), total=bytecount,
324 324 unit=_('bytes'))
325 325 progress.update(0)
326 326
327 327 for chunk in it:
328 328 progress.increment(step=len(chunk))
329 329 yield chunk
330 330
331 331 progress.complete()
332 332
333 333 return requirements, gen()
334 334
335 335 def consumev1(repo, fp, filecount, bytecount):
336 336 """Apply the contents from version 1 of a streaming clone file handle.
337 337
338 338 This takes the output from "stream_out" and applies it to the specified
339 339 repository.
340 340
341 341 Like "stream_out," the status line added by the wire protocol is not
342 342 handled by this function.
343 343 """
344 344 with repo.lock():
345 345 repo.ui.status(_('%d files to transfer, %s of data\n') %
346 346 (filecount, util.bytecount(bytecount)))
347 347 progress = repo.ui.makeprogress(_('clone'), total=bytecount,
348 348 unit=_('bytes'))
349 349 progress.update(0)
350 350 start = util.timer()
351 351
352 352 # TODO: get rid of (potential) inconsistency
353 353 #
354 354 # If transaction is started and any @filecache property is
355 355 # changed at this point, it causes inconsistency between
356 356 # in-memory cached property and streamclone-ed file on the
357 357 # disk. Nested transaction prevents transaction scope "clone"
358 358 # below from writing in-memory changes out at the end of it,
359 359 # even though in-memory changes are discarded at the end of it
360 360 # regardless of transaction nesting.
361 361 #
362 362 # But transaction nesting can't be simply prohibited, because
363 363 # nesting occurs also in ordinary case (e.g. enabling
364 364 # clonebundles).
365 365
366 366 with repo.transaction('clone'):
367 367 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
368 368 for i in pycompat.xrange(filecount):
369 369 # XXX doesn't support '\n' or '\r' in filenames
370 370 l = fp.readline()
371 371 try:
372 372 name, size = l.split('\0', 1)
373 373 size = int(size)
374 374 except (ValueError, TypeError):
375 375 raise error.ResponseError(
376 376 _('unexpected response from remote server:'), l)
377 377 if repo.ui.debugflag:
378 378 repo.ui.debug('adding %s (%s)\n' %
379 379 (name, util.bytecount(size)))
380 380 # for backwards compat, name was partially encoded
381 381 path = store.decodedir(name)
382 382 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
383 383 for chunk in util.filechunkiter(fp, limit=size):
384 384 progress.increment(step=len(chunk))
385 385 ofp.write(chunk)
386 386
387 387 # force @filecache properties to be reloaded from
388 388 # streamclone-ed file at next access
389 389 repo.invalidate(clearfilecache=True)
390 390
391 391 elapsed = util.timer() - start
392 392 if elapsed <= 0:
393 393 elapsed = 0.001
394 394 progress.complete()
395 395 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
396 396 (util.bytecount(bytecount), elapsed,
397 397 util.bytecount(bytecount / elapsed)))
398 398
399 399 def readbundle1header(fp):
400 400 compression = fp.read(2)
401 401 if compression != 'UN':
402 402 raise error.Abort(_('only uncompressed stream clone bundles are '
403 403 'supported; got %s') % compression)
404 404
405 405 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
406 406 requireslen = struct.unpack('>H', fp.read(2))[0]
407 407 requires = fp.read(requireslen)
408 408
409 409 if not requires.endswith('\0'):
410 410 raise error.Abort(_('malformed stream clone bundle: '
411 411 'requirements not properly encoded'))
412 412
413 413 requirements = set(requires.rstrip('\0').split(','))
414 414
415 415 return filecount, bytecount, requirements
416 416
417 417 def applybundlev1(repo, fp):
418 418 """Apply the content from a stream clone bundle version 1.
419 419
420 420 We assume the 4 byte header has been read and validated and the file handle
421 421 is at the 2 byte compression identifier.
422 422 """
423 423 if len(repo):
424 424 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
425 425 'repo'))
426 426
427 427 filecount, bytecount, requirements = readbundle1header(fp)
428 428 missingreqs = requirements - repo.supportedformats
429 429 if missingreqs:
430 430 raise error.Abort(_('unable to apply stream clone: '
431 431 'unsupported format: %s') %
432 432 ', '.join(sorted(missingreqs)))
433 433
434 434 consumev1(repo, fp, filecount, bytecount)
435 435
436 436 class streamcloneapplier(object):
437 437 """Class to manage applying streaming clone bundles.
438 438
439 439 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
440 440 readers to perform bundle type-specific functionality.
441 441 """
442 442 def __init__(self, fh):
443 443 self._fh = fh
444 444
445 445 def apply(self, repo):
446 446 return applybundlev1(repo, self._fh)
447 447
448 448 # type of file to stream
449 449 _fileappend = 0 # append only file
450 450 _filefull = 1 # full snapshot file
451 451
452 452 # Source of the file
453 453 _srcstore = 's' # store (svfs)
454 454 _srccache = 'c' # cache (cache)
455 455
456 456 # This is it's own function so extensions can override it.
457 457 def _walkstreamfullstorefiles(repo):
458 458 """list snapshot file from the store"""
459 459 fnames = []
460 460 if not repo.publishing():
461 461 fnames.append('phaseroots')
462 462 return fnames
463 463
464 464 def _filterfull(entry, copy, vfsmap):
465 465 """actually copy the snapshot files"""
466 466 src, name, ftype, data = entry
467 467 if ftype != _filefull:
468 468 return entry
469 469 return (src, name, ftype, copy(vfsmap[src].join(name)))
470 470
471 471 @contextlib.contextmanager
472 472 def maketempcopies():
473 473 """return a function to temporary copy file"""
474 474 files = []
475 475 try:
476 476 def copy(src):
477 477 fd, dst = pycompat.mkstemp()
478 478 os.close(fd)
479 479 files.append(dst)
480 480 util.copyfiles(src, dst, hardlink=True)
481 481 return dst
482 482 yield copy
483 483 finally:
484 484 for tmp in files:
485 485 util.tryunlink(tmp)
486 486
487 487 def _makemap(repo):
488 488 """make a (src -> vfs) map for the repo"""
489 489 vfsmap = {
490 490 _srcstore: repo.svfs,
491 491 _srccache: repo.cachevfs,
492 492 }
493 493 # we keep repo.vfs out of the on purpose, ther are too many danger there
494 494 # (eg: .hg/hgrc)
495 495 assert repo.vfs not in vfsmap.values()
496 496
497 497 return vfsmap
498 498
499 499 def _emit2(repo, entries, totalfilesize):
500 500 """actually emit the stream bundle"""
501 501 vfsmap = _makemap(repo)
502 502 progress = repo.ui.makeprogress(_('bundle'), total=totalfilesize,
503 503 unit=_('bytes'))
504 504 progress.update(0)
505 505 with maketempcopies() as copy, progress:
506 506 # copy is delayed until we are in the try
507 507 entries = [_filterfull(e, copy, vfsmap) for e in entries]
508 508 yield None # this release the lock on the repository
509 509 seen = 0
510 510
511 511 for src, name, ftype, data in entries:
512 512 vfs = vfsmap[src]
513 513 yield src
514 514 yield util.uvarintencode(len(name))
515 515 if ftype == _fileappend:
516 516 fp = vfs(name)
517 517 size = data
518 518 elif ftype == _filefull:
519 519 fp = open(data, 'rb')
520 520 size = util.fstat(fp).st_size
521 521 try:
522 522 yield util.uvarintencode(size)
523 523 yield name
524 524 if size <= 65536:
525 525 chunks = (fp.read(size),)
526 526 else:
527 527 chunks = util.filechunkiter(fp, limit=size)
528 528 for chunk in chunks:
529 529 seen += len(chunk)
530 530 progress.update(seen)
531 531 yield chunk
532 532 finally:
533 533 fp.close()
534 534
535 535 def generatev2(repo, includes, excludes, includeobsmarkers):
536 536 """Emit content for version 2 of a streaming clone.
537 537
538 538 the data stream consists the following entries:
539 539 1) A char representing the file destination (eg: store or cache)
540 540 2) A varint containing the length of the filename
541 541 3) A varint containing the length of file data
542 542 4) N bytes containing the filename (the internal, store-agnostic form)
543 543 5) N bytes containing the file data
544 544
545 545 Returns a 3-tuple of (file count, file size, data iterator).
546 546 """
547 547
548 # temporarily raise error until we add storage level logic
549 if includes or excludes:
550 raise error.Abort(_("server does not support narrow stream clones"))
551
552 548 with repo.lock():
553 549
554 550 entries = []
555 551 totalfilesize = 0
556 552
557 553 matcher = None
558 554 if includes or excludes:
559 555 matcher = narrowspec.match(repo.root, includes, excludes)
560 556
561 557 repo.ui.debug('scanning\n')
562 558 for name, ename, size in _walkstreamfiles(repo, matcher):
563 559 if size:
564 560 entries.append((_srcstore, name, _fileappend, size))
565 561 totalfilesize += size
566 562 for name in _walkstreamfullstorefiles(repo):
567 563 if repo.svfs.exists(name):
568 564 totalfilesize += repo.svfs.lstat(name).st_size
569 565 entries.append((_srcstore, name, _filefull, None))
570 566 if includeobsmarkers and repo.svfs.exists('obsstore'):
571 567 totalfilesize += repo.svfs.lstat('obsstore').st_size
572 568 entries.append((_srcstore, 'obsstore', _filefull, None))
573 569 for name in cacheutil.cachetocopy(repo):
574 570 if repo.cachevfs.exists(name):
575 571 totalfilesize += repo.cachevfs.lstat(name).st_size
576 572 entries.append((_srccache, name, _filefull, None))
577 573
578 574 chunks = _emit2(repo, entries, totalfilesize)
579 575 first = next(chunks)
580 576 assert first is None
581 577
582 578 return len(entries), totalfilesize, chunks
583 579
584 580 @contextlib.contextmanager
585 581 def nested(*ctxs):
586 582 this = ctxs[0]
587 583 rest = ctxs[1:]
588 584 with this:
589 585 if rest:
590 586 with nested(*rest):
591 587 yield
592 588 else:
593 589 yield
594 590
595 591 def consumev2(repo, fp, filecount, filesize):
596 592 """Apply the contents from a version 2 streaming clone.
597 593
598 594 Data is read from an object that only needs to provide a ``read(size)``
599 595 method.
600 596 """
601 597 with repo.lock():
602 598 repo.ui.status(_('%d files to transfer, %s of data\n') %
603 599 (filecount, util.bytecount(filesize)))
604 600
605 601 start = util.timer()
606 602 progress = repo.ui.makeprogress(_('clone'), total=filesize,
607 603 unit=_('bytes'))
608 604 progress.update(0)
609 605
610 606 vfsmap = _makemap(repo)
611 607
612 608 with repo.transaction('clone'):
613 609 ctxs = (vfs.backgroundclosing(repo.ui)
614 610 for vfs in vfsmap.values())
615 611 with nested(*ctxs):
616 612 for i in range(filecount):
617 613 src = util.readexactly(fp, 1)
618 614 vfs = vfsmap[src]
619 615 namelen = util.uvarintdecodestream(fp)
620 616 datalen = util.uvarintdecodestream(fp)
621 617
622 618 name = util.readexactly(fp, namelen)
623 619
624 620 if repo.ui.debugflag:
625 621 repo.ui.debug('adding [%s] %s (%s)\n' %
626 622 (src, name, util.bytecount(datalen)))
627 623
628 624 with vfs(name, 'w') as ofp:
629 625 for chunk in util.filechunkiter(fp, limit=datalen):
630 626 progress.increment(step=len(chunk))
631 627 ofp.write(chunk)
632 628
633 629 # force @filecache properties to be reloaded from
634 630 # streamclone-ed file at next access
635 631 repo.invalidate(clearfilecache=True)
636 632
637 633 elapsed = util.timer() - start
638 634 if elapsed <= 0:
639 635 elapsed = 0.001
640 636 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
641 637 (util.bytecount(progress.pos), elapsed,
642 638 util.bytecount(progress.pos / elapsed)))
643 639 progress.complete()
644 640
645 641 def applybundlev2(repo, fp, filecount, filesize, requirements):
646 642 from . import localrepo
647 643
648 644 missingreqs = [r for r in requirements if r not in repo.supported]
649 645 if missingreqs:
650 646 raise error.Abort(_('unable to apply stream clone: '
651 647 'unsupported format: %s') %
652 648 ', '.join(sorted(missingreqs)))
653 649
654 650 consumev2(repo, fp, filecount, filesize)
655 651
656 652 # new requirements = old non-format requirements +
657 653 # new format-related remote requirements
658 654 # requirements from the streamed-in repository
659 655 repo.requirements = set(requirements) | (
660 656 repo.requirements - repo.supportedformats)
661 657 repo.svfs.options = localrepo.resolvestorevfsoptions(
662 658 repo.ui, repo.requirements, repo.features)
663 659 repo._writerequirements()
@@ -1,39 +1,86
1 #testcases tree flat
2
1 3 Tests narrow stream clones
2 4
3 5 $ . "$TESTDIR/narrow-library.sh"
4 6
7 #if tree
8 $ cat << EOF >> $HGRCPATH
9 > [experimental]
10 > treemanifest = 1
11 > EOF
12 #endif
13
5 14 Server setup
6 15
7 16 $ hg init master
8 17 $ cd master
9 18 $ mkdir dir
10 19 $ mkdir dir/src
11 20 $ cd dir/src
12 21 $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done
13 22
14 23 $ cd ..
15 24 $ mkdir tests
16 25 $ cd tests
17 26 $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done
18 27 $ cd ../../..
19 28
20 29 Trying to stream clone when the server does not support it
21 30
22 31 $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" --stream
23 32 streaming all changes
24 33 remote: abort: server does not support narrow stream clones
25 34 abort: pull failed on remote
26 35 [255]
27 36
28 37 Enable stream clone on the server
29 38
30 $ echo "[server]" >> master/.hg/hgrc
39 $ echo "[experimental.server]" >> master/.hg/hgrc
31 40 $ echo "stream-narrow-clones=True" >> master/.hg/hgrc
32 41
33 42 Cloning a specific file when stream clone is supported
34 43
35 44 $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" --stream
36 45 streaming all changes
37 remote: abort: server does not support narrow stream clones
38 abort: pull failed on remote
39 [255]
46 * files to transfer, * KB of data (glob)
47 transferred * KB in * seconds (* */sec) (glob)
48
49 $ cd narrow
50 $ ls
51 $ hg tracked
52 I path:dir/src/f10
53
54 Making sure we have the correct set of requirements
55
56 $ cat .hg/requires
57 dotencode
58 fncache
59 generaldelta
60 narrowhg-experimental
61 revlogv1
62 store
63 treemanifest (tree !)
64
65 Making sure store has the required files
66
67 $ ls .hg/store/
68 00changelog.i
69 00manifest.i
70 data
71 fncache
72 meta (tree !)
73 narrowspec
74 undo
75 undo.backupfiles
76 undo.phaseroots
77
78 Checking that repository has all the required data and not broken
79
80 $ hg verify
81 checking changesets
82 checking manifests
83 checking directory manifests (tree !)
84 crosschecking files in changesets and manifests
85 checking files
86 checked 40 changesets with 1 changes to 1 files
General Comments 0
You need to be logged in to leave comments. Login now