comparison mercurial/branchmap.py @ 51527:fa9e3976a5a0

branchcache: rework the `filteredhash` logic to be more generic We now have a more flexible `key_hashes` tuple. We duplicated various logic in the V2 and V3 version of the cache as the goal is to start changing the logic for V3 in the next few changesets.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Wed, 06 Mar 2024 02:20:53 +0100
parents a03fa40afd01
children 4141d12de073
comparison
equal deleted inserted replaced
51526:a03fa40afd01 51527:fa9e3976a5a0
23 Optional, 23 Optional,
24 Set, 24 Set,
25 TYPE_CHECKING, 25 TYPE_CHECKING,
26 Tuple, 26 Tuple,
27 Union, 27 Union,
28 cast,
28 ) 29 )
29 30
30 from . import ( 31 from . import (
31 encoding, 32 encoding,
32 error, 33 error,
407 408
408 class _LocalBranchCache(_BaseBranchCache): 409 class _LocalBranchCache(_BaseBranchCache):
409 """base class of branch-map info for a local repo or repoview""" 410 """base class of branch-map info for a local repo or repoview"""
410 411
411 _base_filename = None 412 _base_filename = None
413 _default_key_hashes: Tuple[bytes] = cast(Tuple[bytes], ())
412 414
413 def __init__( 415 def __init__(
414 self, 416 self,
415 repo: "localrepo.localrepository", 417 repo: "localrepo.localrepository",
416 entries: Union[ 418 entries: Union[
417 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]] 419 Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]
418 ] = (), 420 ] = (),
419 tipnode: Optional[bytes] = None, 421 tipnode: Optional[bytes] = None,
420 tiprev: Optional[int] = nullrev, 422 tiprev: Optional[int] = nullrev,
421 filteredhash: Optional[bytes] = None, 423 key_hashes: Optional[Tuple[bytes]] = None,
422 closednodes: Optional[Set[bytes]] = None, 424 closednodes: Optional[Set[bytes]] = None,
423 hasnode: Optional[Callable[[bytes], bool]] = None, 425 hasnode: Optional[Callable[[bytes], bool]] = None,
424 verify_node: bool = False, 426 verify_node: bool = False,
425 inherited: bool = False, 427 inherited: bool = False,
426 ) -> None: 428 ) -> None:
431 if tipnode is None: 433 if tipnode is None:
432 self.tipnode = repo.nullid 434 self.tipnode = repo.nullid
433 else: 435 else:
434 self.tipnode = tipnode 436 self.tipnode = tipnode
435 self.tiprev = tiprev 437 self.tiprev = tiprev
436 self.filteredhash = filteredhash 438 if key_hashes is None:
439 self.key_hashes = self._default_key_hashes
440 else:
441 self.key_hashes = key_hashes
437 self._state = STATE_CLEAN 442 self._state = STATE_CLEAN
438 if inherited: 443 if inherited:
439 self._state = STATE_INHERITED 444 self._state = STATE_INHERITED
440 445
441 super().__init__(repo=repo, entries=entries, closed_nodes=closednodes) 446 super().__init__(repo=repo, entries=entries, closed_nodes=closednodes)
448 # branches for which nodes are verified 453 # branches for which nodes are verified
449 self._verifiedbranches = set() 454 self._verifiedbranches = set()
450 self._hasnode = None 455 self._hasnode = None
451 if self._verify_node: 456 if self._verify_node:
452 self._hasnode = repo.changelog.hasnode 457 self._hasnode = repo.changelog.hasnode
458
459 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
460 raise NotImplementedError
453 461
454 def validfor(self, repo): 462 def validfor(self, repo):
455 """check that cache contents are valid for (a subset of) this repo 463 """check that cache contents are valid for (a subset of) this repo
456 464
457 - False when the order of changesets changed or if we detect a strip. 465 - False when the order of changesets changed or if we detect a strip.
464 return False 472 return False
465 if self.tipnode != node: 473 if self.tipnode != node:
466 # tiprev doesn't correspond to tipnode: repo was stripped, or this 474 # tiprev doesn't correspond to tipnode: repo was stripped, or this
467 # repo has a different order of changesets 475 # repo has a different order of changesets
468 return False 476 return False
469 tiphash = scmutil.combined_filtered_and_obsolete_hash( 477 repo_key_hashes = self._compute_key_hashes(repo)
470 repo,
471 self.tiprev,
472 needobsolete=True,
473 )
474 # hashes don't match if this repo view has a different set of filtered 478 # hashes don't match if this repo view has a different set of filtered
475 # revisions (e.g. due to phase changes) or obsolete revisions (e.g. 479 # revisions (e.g. due to phase changes) or obsolete revisions (e.g.
476 # history was rewritten) 480 # history was rewritten)
477 return self.filteredhash == tiphash 481 return self.key_hashes == repo_key_hashes
478 482
479 @classmethod 483 @classmethod
480 def fromfile(cls, repo): 484 def fromfile(cls, repo):
481 f = None 485 f = None
482 try: 486 try:
511 515
512 return bcache 516 return bcache
513 517
514 @classmethod 518 @classmethod
515 def _load_header(cls, repo, lineiter) -> "dict[str, Any]": 519 def _load_header(cls, repo, lineiter) -> "dict[str, Any]":
516 """parse the head of a branchmap file 520 raise NotImplementedError
517
518 return parameters to pass to a newly created class instance.
519 """
520 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
521 last, lrev = cachekey[:2]
522 last, lrev = bin(last), int(lrev)
523 filteredhash = None
524 if len(cachekey) > 2:
525 filteredhash = bin(cachekey[2])
526 return {
527 "tipnode": last,
528 "tiprev": lrev,
529 "filteredhash": filteredhash,
530 }
531 521
532 def _load_heads(self, repo, lineiter): 522 def _load_heads(self, repo, lineiter):
533 """fully loads the branchcache by reading from the file using the line 523 """fully loads the branchcache by reading from the file using the line
534 iterator passed""" 524 iterator passed"""
535 for line in lineiter: 525 for line in lineiter:
563 # always replaced, so no need to deepcopy until the above remains 553 # always replaced, so no need to deepcopy until the above remains
564 # true. 554 # true.
565 entries=self._entries, 555 entries=self._entries,
566 tipnode=self.tipnode, 556 tipnode=self.tipnode,
567 tiprev=self.tiprev, 557 tiprev=self.tiprev,
568 filteredhash=self.filteredhash, 558 key_hashes=self.key_hashes,
569 closednodes=set(self._closednodes), 559 closednodes=set(self._closednodes),
570 verify_node=self._verify_node, 560 verify_node=self._verify_node,
571 inherited=True, 561 inherited=True,
572 ) 562 )
573 # also copy information about the current verification state 563 # also copy information about the current verification state
619 b"couldn't write branch cache: %s\n" 609 b"couldn't write branch cache: %s\n"
620 % stringutil.forcebytestr(inst) 610 % stringutil.forcebytestr(inst)
621 ) 611 )
622 612
623 def _write_header(self, fp) -> None: 613 def _write_header(self, fp) -> None:
624 """write the branch cache header to a file""" 614 raise NotImplementedError
625 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
626 if self.filteredhash is not None:
627 cachekey.append(hex(self.filteredhash))
628 fp.write(b" ".join(cachekey) + b'\n')
629 615
630 def _write_heads(self, fp) -> int: 616 def _write_heads(self, fp) -> int:
631 """write list of heads to a file 617 """write list of heads to a file
632 618
633 Return the number of heads written.""" 619 Return the number of heads written."""
712 # invalid for the repo. 698 # invalid for the repo.
713 # 699 #
714 # However. we've just updated the cache and we assume it's valid, 700 # However. we've just updated the cache and we assume it's valid,
715 # so let's make the cache key valid as well by recomputing it from 701 # so let's make the cache key valid as well by recomputing it from
716 # the cached data 702 # the cached data
703 self.key_hashes = self._compute_key_hashes(repo)
717 self.filteredhash = scmutil.combined_filtered_and_obsolete_hash( 704 self.filteredhash = scmutil.combined_filtered_and_obsolete_hash(
718 repo, 705 repo,
719 self.tiprev, 706 self.tiprev,
720 needobsolete=True,
721 ) 707 )
722 708
723 self._state = STATE_DIRTY 709 self._state = STATE_DIRTY
724 tr = repo.currenttransaction() 710 tr = repo.currenttransaction()
725 if getattr(tr, 'finalized', True): 711 if getattr(tr, 'finalized', True):
770 branch head closes a branch or not. 756 branch head closes a branch or not.
771 """ 757 """
772 758
773 _base_filename = b"branch2" 759 _base_filename = b"branch2"
774 760
761 @classmethod
762 def _load_header(cls, repo, lineiter) -> "dict[str, Any]":
763 """parse the head of a branchmap file
764
765 return parameters to pass to a newly created class instance.
766 """
767 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
768 last, lrev = cachekey[:2]
769 last, lrev = bin(last), int(lrev)
770 filteredhash = ()
771 if len(cachekey) > 2:
772 filteredhash = (bin(cachekey[2]),)
773 return {
774 "tipnode": last,
775 "tiprev": lrev,
776 "key_hashes": filteredhash,
777 }
778
779 def _write_header(self, fp) -> None:
780 """write the branch cache header to a file"""
781 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
782 if self.key_hashes:
783 cachekey.append(hex(self.key_hashes[0]))
784 fp.write(b" ".join(cachekey) + b'\n')
785
786 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
787 """return the cache key hashes that match this repoview state"""
788 filtered_hash = scmutil.combined_filtered_and_obsolete_hash(
789 repo,
790 self.tiprev,
791 needobsolete=True,
792 )
793 keys: Tuple[bytes] = cast(Tuple[bytes], ())
794 if filtered_hash is not None:
795 keys: Tuple[bytes] = (filtered_hash,)
796 return keys
797
775 798
776 class BranchCacheV3(_LocalBranchCache): 799 class BranchCacheV3(_LocalBranchCache):
777 """a branch cache using version 3 of the format on disk 800 """a branch cache using version 3 of the format on disk
778 801
779 This version is still EXPERIMENTAL and the format is subject to changes. 802 This version is still EXPERIMENTAL and the format is subject to changes.
809 def _write_header(self, fp) -> None: 832 def _write_header(self, fp) -> None:
810 cache_keys = { 833 cache_keys = {
811 b"tip-node": hex(self.tipnode), 834 b"tip-node": hex(self.tipnode),
812 b"tip-rev": b'%d' % self.tiprev, 835 b"tip-rev": b'%d' % self.tiprev,
813 } 836 }
814 if self.filteredhash is not None: 837 if self.key_hashes:
815 cache_keys[b"filtered-hash"] = hex(self.filteredhash) 838 cache_keys[b"filtered-hash"] = hex(self.key_hashes[0])
816 pieces = (b"%s=%s" % i for i in sorted(cache_keys.items())) 839 pieces = (b"%s=%s" % i for i in sorted(cache_keys.items()))
817 fp.write(b" ".join(pieces) + b'\n') 840 fp.write(b" ".join(pieces) + b'\n')
818 841
819 @classmethod 842 @classmethod
820 def _load_header(cls, repo, lineiter): 843 def _load_header(cls, repo, lineiter):
827 if k == b"tip-rev": 850 if k == b"tip-rev":
828 args["tiprev"] = int(v) 851 args["tiprev"] = int(v)
829 elif k == b"tip-node": 852 elif k == b"tip-node":
830 args["tipnode"] = bin(v) 853 args["tipnode"] = bin(v)
831 elif k == b"filtered-hash": 854 elif k == b"filtered-hash":
832 args["filteredhash"] = bin(v) 855 args["key_hashes"] = (bin(v),)
833 else: 856 else:
834 msg = b"unknown cache key: %r" % k 857 msg = b"unknown cache key: %r" % k
835 raise ValueError(msg) 858 raise ValueError(msg)
836 return args 859 return args
860
861 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
862 """return the cache key hashes that match this repoview state"""
863 filtered_hash = scmutil.combined_filtered_and_obsolete_hash(
864 repo,
865 self.tiprev,
866 needobsolete=True,
867 )
868 if filtered_hash is None:
869 return cast(Tuple[bytes], ())
870 else:
871 return (filtered_hash,)
837 872
838 873
839 class remotebranchcache(_BaseBranchCache): 874 class remotebranchcache(_BaseBranchCache):
840 """Branchmap info for a remote connection, should not write locally""" 875 """Branchmap info for a remote connection, should not write locally"""
841 876