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