##// END OF EJS Templates
tests: byteify a few more things in simplestorerepo.py...
Matt Harbison -
r44132:8ca92bcb default
parent child Browse files
Show More
@@ -1,735 +1,735 b''
1 1 # simplestorerepo.py - Extension that swaps in alternate repository storage.
2 2 #
3 3 # Copyright 2018 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 # To use this with the test suite:
9 9 #
10 10 # $ HGREPOFEATURES="simplestore" ./run-tests.py \
11 11 # --extra-config-opt extensions.simplestore=`pwd`/simplestorerepo.py
12 12
13 13 from __future__ import absolute_import
14 14
15 15 import stat
16 16
17 17 from mercurial.i18n import _
18 18 from mercurial.node import (
19 19 bin,
20 20 hex,
21 21 nullid,
22 22 nullrev,
23 23 )
24 24 from mercurial.thirdparty import attr
25 25 from mercurial import (
26 26 ancestor,
27 27 bundlerepo,
28 28 error,
29 29 extensions,
30 30 localrepo,
31 31 mdiff,
32 32 pycompat,
33 33 revlog,
34 34 store,
35 35 verify,
36 36 )
37 37 from mercurial.interfaces import (
38 38 repository,
39 39 util as interfaceutil,
40 40 )
41 41 from mercurial.utils import (
42 42 cborutil,
43 43 storageutil,
44 44 )
45 45 from mercurial.revlogutils import flagutil
46 46
47 47 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
48 48 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
49 49 # be specifying the version(s) of Mercurial they are tested with, or
50 50 # leave the attribute unspecified.
51 testedwith = 'ships-with-hg-core'
51 testedwith = b'ships-with-hg-core'
52 52
53 REQUIREMENT = 'testonly-simplestore'
53 REQUIREMENT = b'testonly-simplestore'
54 54
55 55
56 56 def validatenode(node):
57 57 if isinstance(node, int):
58 58 raise ValueError('expected node; got int')
59 59
60 60 if len(node) != 20:
61 61 raise ValueError('expected 20 byte node')
62 62
63 63
64 64 def validaterev(rev):
65 65 if not isinstance(rev, int):
66 66 raise ValueError('expected int')
67 67
68 68
69 69 class simplestoreerror(error.StorageError):
70 70 pass
71 71
72 72
73 73 @interfaceutil.implementer(repository.irevisiondelta)
74 74 @attr.s(slots=True)
75 75 class simplestorerevisiondelta(object):
76 76 node = attr.ib()
77 77 p1node = attr.ib()
78 78 p2node = attr.ib()
79 79 basenode = attr.ib()
80 80 flags = attr.ib()
81 81 baserevisionsize = attr.ib()
82 82 revision = attr.ib()
83 83 delta = attr.ib()
84 84 linknode = attr.ib(default=None)
85 85
86 86
87 87 @interfaceutil.implementer(repository.iverifyproblem)
88 88 @attr.s(frozen=True)
89 89 class simplefilestoreproblem(object):
90 90 warning = attr.ib(default=None)
91 91 error = attr.ib(default=None)
92 92 node = attr.ib(default=None)
93 93
94 94
95 95 @interfaceutil.implementer(repository.ifilestorage)
96 96 class filestorage(object):
97 97 """Implements storage for a tracked path.
98 98
99 99 Data is stored in the VFS in a directory corresponding to the tracked
100 100 path.
101 101
102 102 Index data is stored in an ``index`` file using CBOR.
103 103
104 104 Fulltext data is stored in files having names of the node.
105 105 """
106 106
107 107 _flagserrorclass = simplestoreerror
108 108
109 109 def __init__(self, svfs, path):
110 110 self._svfs = svfs
111 111 self._path = path
112 112
113 113 self._storepath = b'/'.join([b'data', path])
114 114 self._indexpath = b'/'.join([self._storepath, b'index'])
115 115
116 116 indexdata = self._svfs.tryread(self._indexpath)
117 117 if indexdata:
118 118 indexdata = cborutil.decodeall(indexdata)
119 119
120 120 self._indexdata = indexdata or []
121 121 self._indexbynode = {}
122 122 self._indexbyrev = {}
123 123 self._index = []
124 124 self._refreshindex()
125 125
126 126 self._flagprocessors = dict(flagutil.flagprocessors)
127 127
128 128 def _refreshindex(self):
129 129 self._indexbynode.clear()
130 130 self._indexbyrev.clear()
131 131 self._index = []
132 132
133 133 for i, entry in enumerate(self._indexdata):
134 134 self._indexbynode[entry[b'node']] = entry
135 135 self._indexbyrev[i] = entry
136 136
137 137 self._indexbynode[nullid] = {
138 138 b'node': nullid,
139 139 b'p1': nullid,
140 140 b'p2': nullid,
141 141 b'linkrev': nullrev,
142 142 b'flags': 0,
143 143 }
144 144
145 145 self._indexbyrev[nullrev] = {
146 146 b'node': nullid,
147 147 b'p1': nullid,
148 148 b'p2': nullid,
149 149 b'linkrev': nullrev,
150 150 b'flags': 0,
151 151 }
152 152
153 153 for i, entry in enumerate(self._indexdata):
154 154 p1rev, p2rev = self.parentrevs(self.rev(entry[b'node']))
155 155
156 156 # start, length, rawsize, chainbase, linkrev, p1, p2, node
157 157 self._index.append(
158 158 (0, 0, 0, -1, entry[b'linkrev'], p1rev, p2rev, entry[b'node'])
159 159 )
160 160
161 161 self._index.append((0, 0, 0, -1, -1, -1, -1, nullid))
162 162
163 163 def __len__(self):
164 164 return len(self._indexdata)
165 165
166 166 def __iter__(self):
167 167 return iter(range(len(self)))
168 168
169 169 def revs(self, start=0, stop=None):
170 170 step = 1
171 171 if stop is not None:
172 172 if start > stop:
173 173 step = -1
174 174
175 175 stop += step
176 176 else:
177 177 stop = len(self)
178 178
179 179 return range(start, stop, step)
180 180
181 181 def parents(self, node):
182 182 validatenode(node)
183 183
184 184 if node not in self._indexbynode:
185 185 raise KeyError('unknown node')
186 186
187 187 entry = self._indexbynode[node]
188 188
189 189 return entry[b'p1'], entry[b'p2']
190 190
191 191 def parentrevs(self, rev):
192 192 p1, p2 = self.parents(self._indexbyrev[rev][b'node'])
193 193 return self.rev(p1), self.rev(p2)
194 194
195 195 def rev(self, node):
196 196 validatenode(node)
197 197
198 198 try:
199 199 self._indexbynode[node]
200 200 except KeyError:
201 201 raise error.LookupError(node, self._indexpath, _('no node'))
202 202
203 203 for rev, entry in self._indexbyrev.items():
204 204 if entry[b'node'] == node:
205 205 return rev
206 206
207 raise error.ProgrammingError('this should not occur')
207 raise error.ProgrammingError(b'this should not occur')
208 208
209 209 def node(self, rev):
210 210 validaterev(rev)
211 211
212 212 return self._indexbyrev[rev][b'node']
213 213
214 214 def hasnode(self, node):
215 215 validatenode(node)
216 216 return node in self._indexbynode
217 217
218 218 def censorrevision(self, tr, censornode, tombstone=b''):
219 219 raise NotImplementedError('TODO')
220 220
221 221 def lookup(self, node):
222 222 if isinstance(node, int):
223 223 return self.node(node)
224 224
225 225 if len(node) == 20:
226 226 self.rev(node)
227 227 return node
228 228
229 229 try:
230 230 rev = int(node)
231 231 if '%d' % rev != node:
232 232 raise ValueError
233 233
234 234 if rev < 0:
235 235 rev = len(self) + rev
236 236 if rev < 0 or rev >= len(self):
237 237 raise ValueError
238 238
239 239 return self.node(rev)
240 240 except (ValueError, OverflowError):
241 241 pass
242 242
243 243 if len(node) == 40:
244 244 try:
245 245 rawnode = bin(node)
246 246 self.rev(rawnode)
247 247 return rawnode
248 248 except TypeError:
249 249 pass
250 250
251 251 raise error.LookupError(node, self._path, _('invalid lookup input'))
252 252
253 253 def linkrev(self, rev):
254 254 validaterev(rev)
255 255
256 256 return self._indexbyrev[rev][b'linkrev']
257 257
258 258 def _flags(self, rev):
259 259 validaterev(rev)
260 260
261 261 return self._indexbyrev[rev][b'flags']
262 262
263 263 def _candelta(self, baserev, rev):
264 264 validaterev(baserev)
265 265 validaterev(rev)
266 266
267 267 if (self._flags(baserev) & revlog.REVIDX_RAWTEXT_CHANGING_FLAGS) or (
268 268 self._flags(rev) & revlog.REVIDX_RAWTEXT_CHANGING_FLAGS
269 269 ):
270 270 return False
271 271
272 272 return True
273 273
274 274 def checkhash(self, text, node, p1=None, p2=None, rev=None):
275 275 if p1 is None and p2 is None:
276 276 p1, p2 = self.parents(node)
277 277 if node != storageutil.hashrevisionsha1(text, p1, p2):
278 278 raise simplestoreerror(
279 279 _("integrity check failed on %s") % self._path
280 280 )
281 281
282 282 def revision(self, nodeorrev, raw=False):
283 283 if isinstance(nodeorrev, int):
284 284 node = self.node(nodeorrev)
285 285 else:
286 286 node = nodeorrev
287 287 validatenode(node)
288 288
289 289 if node == nullid:
290 290 return b''
291 291
292 292 rev = self.rev(node)
293 293 flags = self._flags(rev)
294 294
295 295 path = b'/'.join([self._storepath, hex(node)])
296 296 rawtext = self._svfs.read(path)
297 297
298 298 if raw:
299 299 validatehash = flagutil.processflagsraw(self, rawtext, flags)
300 300 text = rawtext
301 301 else:
302 302 r = flagutil.processflagsread(self, rawtext, flags)
303 303 text, validatehash, sidedata = r
304 304 if validatehash:
305 305 self.checkhash(text, node, rev=rev)
306 306
307 307 return text
308 308
309 309 def rawdata(self, nodeorrev):
310 310 return self.revision(raw=True)
311 311
312 312 def read(self, node):
313 313 validatenode(node)
314 314
315 315 revision = self.revision(node)
316 316
317 317 if not revision.startswith(b'\1\n'):
318 318 return revision
319 319
320 320 start = revision.index(b'\1\n', 2)
321 321 return revision[start + 2 :]
322 322
323 323 def renamed(self, node):
324 324 validatenode(node)
325 325
326 326 if self.parents(node)[0] != nullid:
327 327 return False
328 328
329 329 fulltext = self.revision(node)
330 330 m = storageutil.parsemeta(fulltext)[0]
331 331
332 332 if m and 'copy' in m:
333 333 return m['copy'], bin(m['copyrev'])
334 334
335 335 return False
336 336
337 337 def cmp(self, node, text):
338 338 validatenode(node)
339 339
340 340 t = text
341 341
342 342 if text.startswith(b'\1\n'):
343 343 t = b'\1\n\1\n' + text
344 344
345 345 p1, p2 = self.parents(node)
346 346
347 347 if storageutil.hashrevisionsha1(t, p1, p2) == node:
348 348 return False
349 349
350 350 if self.iscensored(self.rev(node)):
351 351 return text != b''
352 352
353 353 if self.renamed(node):
354 354 t2 = self.read(node)
355 355 return t2 != text
356 356
357 357 return True
358 358
359 359 def size(self, rev):
360 360 validaterev(rev)
361 361
362 362 node = self._indexbyrev[rev][b'node']
363 363
364 364 if self.renamed(node):
365 365 return len(self.read(node))
366 366
367 367 if self.iscensored(rev):
368 368 return 0
369 369
370 370 return len(self.revision(node))
371 371
372 372 def iscensored(self, rev):
373 373 validaterev(rev)
374 374
375 375 return self._flags(rev) & repository.REVISION_FLAG_CENSORED
376 376
377 377 def commonancestorsheads(self, a, b):
378 378 validatenode(a)
379 379 validatenode(b)
380 380
381 381 a = self.rev(a)
382 382 b = self.rev(b)
383 383
384 384 ancestors = ancestor.commonancestorsheads(self.parentrevs, a, b)
385 385 return pycompat.maplist(self.node, ancestors)
386 386
387 387 def descendants(self, revs):
388 388 # This is a copy of revlog.descendants()
389 389 first = min(revs)
390 390 if first == nullrev:
391 391 for i in self:
392 392 yield i
393 393 return
394 394
395 395 seen = set(revs)
396 396 for i in self.revs(start=first + 1):
397 397 for x in self.parentrevs(i):
398 398 if x != nullrev and x in seen:
399 399 seen.add(i)
400 400 yield i
401 401 break
402 402
403 403 # Required by verify.
404 404 def files(self):
405 405 entries = self._svfs.listdir(self._storepath)
406 406
407 407 # Strip out undo.backup.* files created as part of transaction
408 408 # recording.
409 409 entries = [f for f in entries if not f.startswith('undo.backup.')]
410 410
411 411 return [b'/'.join((self._storepath, f)) for f in entries]
412 412
413 413 def storageinfo(
414 414 self,
415 415 exclusivefiles=False,
416 416 sharedfiles=False,
417 417 revisionscount=False,
418 418 trackedsize=False,
419 419 storedsize=False,
420 420 ):
421 421 # TODO do a real implementation of this
422 422 return {
423 423 'exclusivefiles': [],
424 424 'sharedfiles': [],
425 425 'revisionscount': len(self),
426 426 'trackedsize': 0,
427 427 'storedsize': None,
428 428 }
429 429
430 430 def verifyintegrity(self, state):
431 431 state['skipread'] = set()
432 432 for rev in self:
433 433 node = self.node(rev)
434 434 try:
435 435 self.revision(node)
436 436 except Exception as e:
437 437 yield simplefilestoreproblem(
438 438 error='unpacking %s: %s' % (node, e), node=node
439 439 )
440 440 state['skipread'].add(node)
441 441
442 442 def emitrevisions(
443 443 self,
444 444 nodes,
445 445 nodesorder=None,
446 446 revisiondata=False,
447 447 assumehaveparentrevisions=False,
448 448 deltamode=repository.CG_DELTAMODE_STD,
449 449 ):
450 450 # TODO this will probably break on some ordering options.
451 451 nodes = [n for n in nodes if n != nullid]
452 452 if not nodes:
453 453 return
454 454 for delta in storageutil.emitrevisions(
455 455 self,
456 456 nodes,
457 457 nodesorder,
458 458 simplestorerevisiondelta,
459 459 revisiondata=revisiondata,
460 460 assumehaveparentrevisions=assumehaveparentrevisions,
461 461 deltamode=deltamode,
462 462 ):
463 463 yield delta
464 464
465 465 def add(self, text, meta, transaction, linkrev, p1, p2):
466 466 if meta or text.startswith(b'\1\n'):
467 467 text = storageutil.packmeta(meta, text)
468 468
469 469 return self.addrevision(text, transaction, linkrev, p1, p2)
470 470
471 471 def addrevision(
472 472 self,
473 473 text,
474 474 transaction,
475 475 linkrev,
476 476 p1,
477 477 p2,
478 478 node=None,
479 479 flags=revlog.REVIDX_DEFAULT_FLAGS,
480 480 cachedelta=None,
481 481 ):
482 482 validatenode(p1)
483 483 validatenode(p2)
484 484
485 485 if flags:
486 486 node = node or storageutil.hashrevisionsha1(text, p1, p2)
487 487
488 488 rawtext, validatehash = flagutil.processflagswrite(self, text, flags)
489 489
490 490 node = node or storageutil.hashrevisionsha1(text, p1, p2)
491 491
492 492 if node in self._indexbynode:
493 493 return node
494 494
495 495 if validatehash:
496 496 self.checkhash(rawtext, node, p1=p1, p2=p2)
497 497
498 498 return self._addrawrevision(
499 499 node, rawtext, transaction, linkrev, p1, p2, flags
500 500 )
501 501
502 502 def _addrawrevision(self, node, rawtext, transaction, link, p1, p2, flags):
503 503 transaction.addbackup(self._indexpath)
504 504
505 505 path = b'/'.join([self._storepath, hex(node)])
506 506
507 507 self._svfs.write(path, rawtext)
508 508
509 509 self._indexdata.append(
510 510 {
511 511 b'node': node,
512 512 b'p1': p1,
513 513 b'p2': p2,
514 514 b'linkrev': link,
515 515 b'flags': flags,
516 516 }
517 517 )
518 518
519 519 self._reflectindexupdate()
520 520
521 521 return node
522 522
523 523 def _reflectindexupdate(self):
524 524 self._refreshindex()
525 525 self._svfs.write(
526 526 self._indexpath, ''.join(cborutil.streamencode(self._indexdata))
527 527 )
528 528
529 529 def addgroup(
530 530 self,
531 531 deltas,
532 532 linkmapper,
533 533 transaction,
534 534 addrevisioncb=None,
535 535 maybemissingparents=False,
536 536 ):
537 537 if maybemissingparents:
538 538 raise error.Abort(
539 539 _('simple store does not support missing parents ' 'write mode')
540 540 )
541 541
542 542 nodes = []
543 543
544 544 transaction.addbackup(self._indexpath)
545 545
546 546 for node, p1, p2, linknode, deltabase, delta, flags in deltas:
547 547 linkrev = linkmapper(linknode)
548 548 flags = flags or revlog.REVIDX_DEFAULT_FLAGS
549 549
550 550 nodes.append(node)
551 551
552 552 if node in self._indexbynode:
553 553 continue
554 554
555 555 # Need to resolve the fulltext from the delta base.
556 556 if deltabase == nullid:
557 557 text = mdiff.patch(b'', delta)
558 558 else:
559 559 text = mdiff.patch(self.revision(deltabase), delta)
560 560
561 561 self._addrawrevision(
562 562 node, text, transaction, linkrev, p1, p2, flags
563 563 )
564 564
565 565 if addrevisioncb:
566 566 addrevisioncb(self, node)
567 567 return nodes
568 568
569 569 def _headrevs(self):
570 570 # Assume all revisions are heads by default.
571 571 revishead = {rev: True for rev in self._indexbyrev}
572 572
573 573 for rev, entry in self._indexbyrev.items():
574 574 # Unset head flag for all seen parents.
575 575 revishead[self.rev(entry[b'p1'])] = False
576 576 revishead[self.rev(entry[b'p2'])] = False
577 577
578 578 return [rev for rev, ishead in sorted(revishead.items()) if ishead]
579 579
580 580 def heads(self, start=None, stop=None):
581 581 # This is copied from revlog.py.
582 582 if start is None and stop is None:
583 583 if not len(self):
584 584 return [nullid]
585 585 return [self.node(r) for r in self._headrevs()]
586 586
587 587 if start is None:
588 588 start = nullid
589 589 if stop is None:
590 590 stop = []
591 591 stoprevs = set([self.rev(n) for n in stop])
592 592 startrev = self.rev(start)
593 593 reachable = {startrev}
594 594 heads = {startrev}
595 595
596 596 parentrevs = self.parentrevs
597 597 for r in self.revs(start=startrev + 1):
598 598 for p in parentrevs(r):
599 599 if p in reachable:
600 600 if r not in stoprevs:
601 601 reachable.add(r)
602 602 heads.add(r)
603 603 if p in heads and p not in stoprevs:
604 604 heads.remove(p)
605 605
606 606 return [self.node(r) for r in heads]
607 607
608 608 def children(self, node):
609 609 validatenode(node)
610 610
611 611 # This is a copy of revlog.children().
612 612 c = []
613 613 p = self.rev(node)
614 614 for r in self.revs(start=p + 1):
615 615 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
616 616 if prevs:
617 617 for pr in prevs:
618 618 if pr == p:
619 619 c.append(self.node(r))
620 620 elif p == nullrev:
621 621 c.append(self.node(r))
622 622 return c
623 623
624 624 def getstrippoint(self, minlink):
625 625 return storageutil.resolvestripinfo(
626 626 minlink,
627 627 len(self) - 1,
628 628 self._headrevs(),
629 629 self.linkrev,
630 630 self.parentrevs,
631 631 )
632 632
633 633 def strip(self, minlink, transaction):
634 634 if not len(self):
635 635 return
636 636
637 637 rev, _ignored = self.getstrippoint(minlink)
638 638 if rev == len(self):
639 639 return
640 640
641 641 # Purge index data starting at the requested revision.
642 642 self._indexdata[rev:] = []
643 643 self._reflectindexupdate()
644 644
645 645
646 646 def issimplestorefile(f, kind, st):
647 647 if kind != stat.S_IFREG:
648 648 return False
649 649
650 650 if store.isrevlog(f, kind, st):
651 651 return False
652 652
653 653 # Ignore transaction undo files.
654 654 if f.startswith('undo.'):
655 655 return False
656 656
657 657 # Otherwise assume it belongs to the simple store.
658 658 return True
659 659
660 660
661 661 class simplestore(store.encodedstore):
662 662 def datafiles(self):
663 663 for x in super(simplestore, self).datafiles():
664 664 yield x
665 665
666 666 # Supplement with non-revlog files.
667 667 extrafiles = self._walk('data', True, filefilter=issimplestorefile)
668 668
669 669 for unencoded, encoded, size in extrafiles:
670 670 try:
671 671 unencoded = store.decodefilename(unencoded)
672 672 except KeyError:
673 673 unencoded = None
674 674
675 675 yield unencoded, encoded, size
676 676
677 677
678 678 def reposetup(ui, repo):
679 679 if not repo.local():
680 680 return
681 681
682 682 if isinstance(repo, bundlerepo.bundlerepository):
683 683 raise error.Abort(_('cannot use simple store with bundlerepo'))
684 684
685 685 class simplestorerepo(repo.__class__):
686 686 def file(self, f):
687 687 return filestorage(self.svfs, f)
688 688
689 689 repo.__class__ = simplestorerepo
690 690
691 691
692 692 def featuresetup(ui, supported):
693 693 supported.add(REQUIREMENT)
694 694
695 695
696 696 def newreporequirements(orig, ui, createopts):
697 697 """Modifies default requirements for new repos to use the simple store."""
698 698 requirements = orig(ui, createopts)
699 699
700 700 # These requirements are only used to affect creation of the store
701 701 # object. We have our own store. So we can remove them.
702 702 # TODO do this once we feel like taking the test hit.
703 703 # if 'fncache' in requirements:
704 704 # requirements.remove('fncache')
705 705 # if 'dotencode' in requirements:
706 706 # requirements.remove('dotencode')
707 707
708 708 requirements.add(REQUIREMENT)
709 709
710 710 return requirements
711 711
712 712
713 713 def makestore(orig, requirements, path, vfstype):
714 714 if REQUIREMENT not in requirements:
715 715 return orig(requirements, path, vfstype)
716 716
717 717 return simplestore(path, vfstype)
718 718
719 719
720 720 def verifierinit(orig, self, *args, **kwargs):
721 721 orig(self, *args, **kwargs)
722 722
723 723 # We don't care that files in the store don't align with what is
724 724 # advertised. So suppress these warnings.
725 725 self.warnorphanstorefiles = False
726 726
727 727
728 728 def extsetup(ui):
729 729 localrepo.featuresetupfuncs.add(featuresetup)
730 730
731 731 extensions.wrapfunction(
732 732 localrepo, 'newreporequirements', newreporequirements
733 733 )
734 734 extensions.wrapfunction(localrepo, 'makestore', makestore)
735 735 extensions.wrapfunction(verify.verifier, '__init__', verifierinit)
General Comments 0
You need to be logged in to leave comments. Login now