comparison mercurial/wireprotov2server.py @ 40176:41263df08109

wireprotov2: change how revisions are specified to changesetdata Right now, we have a handful of arguments for specifying the revisions whose data should be returned. Defining how all these arguments interact when various combinations are present is difficult. This commit establishes a new, generic mechanism for specifying revisions. Instead of a hodgepodge of arguments defining things, we have a list of dicts that specify revision selectors. The final set of revisions is a union of all these selectors. We implement support for specifying revisions based on: * An explicit list of changeset revisions * An explicit list of changeset revisions plus ancestry depth * A DAG range between changeset roots and heads If you squint hard enough, this problem has already been solved by revsets. But I'm reluctant to expose revsets to the wire protocol because that would require servers to implement a revset parser. Plus there are security and performance implications: the set of revision selectors needs to be narrowly and specifically tailored for what is appropriate to be executing on a server. Perhaps there would be a way for us to express the "parse tree" of a revset query, for example. I'm not sure. We can explore this space another time. For now, the new mechanism should bring sufficient flexibility while remaining relatively simple. The selector "types" are prefixed with "changeset" because I plan to add manifest and file-flavored selectors as well. This will enable us to e.g. select file revisions based on a range of changeset revisions. Differential Revision: https://phab.mercurial-scm.org/D4979
author Gregory Szorc <gregory.szorc@gmail.com>
date Mon, 08 Oct 2018 18:17:12 -0700
parents 6c42409691ec
children 41e2633bcd00
comparison
equal deleted inserted replaced
40175:6c42409691ec 40176:41263df08109
775 Extensions can monkeypatch this function to provide custom caching 775 Extensions can monkeypatch this function to provide custom caching
776 backends. 776 backends.
777 """ 777 """
778 return None 778 return None
779 779
780 def resolvenodes(repo, revisions):
781 """Resolve nodes from a revisions specifier data structure."""
782 cl = repo.changelog
783 clhasnode = cl.hasnode
784
785 seen = set()
786 nodes = []
787
788 if not isinstance(revisions, list):
789 raise error.WireprotoCommandError('revisions must be defined as an '
790 'array')
791
792 for spec in revisions:
793 if b'type' not in spec:
794 raise error.WireprotoCommandError(
795 'type key not present in revision specifier')
796
797 typ = spec[b'type']
798
799 if typ == b'changesetexplicit':
800 if b'nodes' not in spec:
801 raise error.WireprotoCommandError(
802 'nodes key not present in changesetexplicit revision '
803 'specifier')
804
805 for node in spec[b'nodes']:
806 if node not in seen:
807 nodes.append(node)
808 seen.add(node)
809
810 elif typ == b'changesetexplicitdepth':
811 for key in (b'nodes', b'depth'):
812 if key not in spec:
813 raise error.WireprotoCommandError(
814 '%s key not present in changesetexplicitdepth revision '
815 'specifier', (key,))
816
817 for rev in repo.revs(b'ancestors(%ln, %d)', spec[b'nodes'],
818 spec[b'depth'] - 1):
819 node = cl.node(rev)
820
821 if node not in seen:
822 nodes.append(node)
823 seen.add(node)
824
825 elif typ == b'changesetdagrange':
826 for key in (b'roots', b'heads'):
827 if key not in spec:
828 raise error.WireprotoCommandError(
829 '%s key not present in changesetdagrange revision '
830 'specifier', (key,))
831
832 if not spec[b'heads']:
833 raise error.WireprotoCommandError(
834 'heads key in changesetdagrange cannot be empty')
835
836 if spec[b'roots']:
837 common = [n for n in spec[b'roots'] if clhasnode(n)]
838 else:
839 common = [nullid]
840
841 for n in discovery.outgoing(repo, common, spec[b'heads']).missing:
842 if n not in seen:
843 nodes.append(n)
844 seen.add(n)
845
846 else:
847 raise error.WireprotoCommandError(
848 'unknown revision specifier type: %s', (typ,))
849
850 return nodes
851
780 @wireprotocommand('branchmap', permission='pull') 852 @wireprotocommand('branchmap', permission='pull')
781 def branchmapv2(repo, proto): 853 def branchmapv2(repo, proto):
782 yield {encoding.fromlocal(k): v 854 yield {encoding.fromlocal(k): v
783 for k, v in repo.branchmap().iteritems()} 855 for k, v in repo.branchmap().iteritems()}
784 856
787 yield _capabilitiesv2(repo, proto) 859 yield _capabilitiesv2(repo, proto)
788 860
789 @wireprotocommand( 861 @wireprotocommand(
790 'changesetdata', 862 'changesetdata',
791 args={ 863 args={
792 'noderange': { 864 'revisions': {
793 'type': 'list', 865 'type': 'list',
794 'default': lambda: None, 866 'example': [{
795 'example': [[b'0123456...'], [b'abcdef...']], 867 b'type': b'changesetexplicit',
796 }, 868 b'nodes': [b'abcdef...'],
797 'nodes': { 869 }],
798 'type': 'list',
799 'default': lambda: None,
800 'example': [b'0123456...'],
801 },
802 'nodesdepth': {
803 'type': 'int',
804 'default': lambda: None,
805 'example': 10,
806 }, 870 },
807 'fields': { 871 'fields': {
808 'type': 'set', 872 'type': 'set',
809 'default': set, 873 'default': set,
810 'example': {b'parents', b'revision'}, 874 'example': {b'parents', b'revision'},
811 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, 875 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
812 }, 876 },
813 }, 877 },
814 permission='pull') 878 permission='pull')
815 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields): 879 def changesetdata(repo, proto, revisions, fields):
816 # TODO look for unknown fields and abort when they can't be serviced. 880 # TODO look for unknown fields and abort when they can't be serviced.
817 # This could probably be validated by dispatcher using validvalues. 881 # This could probably be validated by dispatcher using validvalues.
818 882
819 if noderange is None and nodes is None:
820 raise error.WireprotoCommandError(
821 'noderange or nodes must be defined')
822
823 if nodesdepth is not None and nodes is None:
824 raise error.WireprotoCommandError(
825 'nodesdepth requires the nodes argument')
826
827 if noderange is not None:
828 if len(noderange) != 2:
829 raise error.WireprotoCommandError(
830 'noderange must consist of 2 elements')
831
832 if not noderange[1]:
833 raise error.WireprotoCommandError(
834 'heads in noderange request cannot be empty')
835
836 cl = repo.changelog 883 cl = repo.changelog
837 hasnode = cl.hasnode 884 outgoing = resolvenodes(repo, revisions)
838
839 seen = set()
840 outgoing = []
841
842 if nodes is not None:
843 outgoing = [n for n in nodes if hasnode(n)]
844
845 if nodesdepth:
846 outgoing = [cl.node(r) for r in
847 repo.revs(b'ancestors(%ln, %d)', outgoing,
848 nodesdepth - 1)]
849
850 seen |= set(outgoing)
851
852 if noderange is not None:
853 if noderange[0]:
854 common = [n for n in noderange[0] if hasnode(n)]
855 else:
856 common = [nullid]
857
858 for n in discovery.outgoing(repo, common, noderange[1]).missing:
859 if n not in seen:
860 outgoing.append(n)
861 # Don't need to add to seen here because this is the final
862 # source of nodes and there should be no duplicates in this
863 # list.
864
865 seen.clear()
866 publishing = repo.publishing() 885 publishing = repo.publishing()
867 886
868 if outgoing: 887 if outgoing:
869 repo.hook('preoutgoing', throw=True, source='serve') 888 repo.hook('preoutgoing', throw=True, source='serve')
870 889