Show More
@@ -0,0 +1,96 | |||||
|
1 | $ . $TESTDIR/wireprotohelpers.sh | |||
|
2 | ||||
|
3 | $ hg init server | |||
|
4 | $ enablehttpv2 server | |||
|
5 | $ cd server | |||
|
6 | $ hg debugdrawdag << EOF | |||
|
7 | > H I J | |||
|
8 | > | | | | |||
|
9 | > E F G | |||
|
10 | > | |/ | |||
|
11 | > C D | |||
|
12 | > |/ | |||
|
13 | > B | |||
|
14 | > | | |||
|
15 | > A | |||
|
16 | > EOF | |||
|
17 | ||||
|
18 | $ hg phase --force --secret J | |||
|
19 | $ hg phase --public E | |||
|
20 | ||||
|
21 | $ hg log -r 'E + H + I + G + J' -T '{rev}:{node} {desc} {phase}\n' | |||
|
22 | 4:78d2dca436b2f5b188ac267e29b81e07266d38fc E public | |||
|
23 | 7:ae492e36b0c8339ffaf328d00b85b4525de1165e H draft | |||
|
24 | 8:1d6f6b91d44aaba6d5e580bc30a9948530dbe00b I draft | |||
|
25 | 6:29446d2dc5419c5f97447a8bc062e4cc328bf241 G draft | |||
|
26 | 9:dec04b246d7cbb670c6689806c05ad17c835284e J secret | |||
|
27 | ||||
|
28 | $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log | |||
|
29 | $ cat hg.pid > $DAEMON_PIDS | |||
|
30 | ||||
|
31 | All non-secret heads returned by default | |||
|
32 | ||||
|
33 | $ sendhttpv2peer << EOF | |||
|
34 | > command heads | |||
|
35 | > EOF | |||
|
36 | creating http peer for wire protocol version 2 | |||
|
37 | sending heads command | |||
|
38 | s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n | |||
|
39 | s> Accept-Encoding: identity\r\n | |||
|
40 | s> accept: application/mercurial-exp-framing-0003\r\n | |||
|
41 | s> content-type: application/mercurial-exp-framing-0003\r\n | |||
|
42 | s> content-length: 20\r\n | |||
|
43 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |||
|
44 | s> user-agent: Mercurial debugwireproto\r\n | |||
|
45 | s> \r\n | |||
|
46 | s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEheads | |||
|
47 | s> makefile('rb', None) | |||
|
48 | s> HTTP/1.1 200 OK\r\n | |||
|
49 | s> Server: testing stub value\r\n | |||
|
50 | s> Date: $HTTP_DATE$\r\n | |||
|
51 | s> Content-Type: application/mercurial-exp-framing-0003\r\n | |||
|
52 | s> Transfer-Encoding: chunked\r\n | |||
|
53 | s> \r\n | |||
|
54 | s> 48\r\n | |||
|
55 | s> @\x00\x00\x01\x00\x02\x01F | |||
|
56 | s> \x83T\x1dok\x91\xd4J\xab\xa6\xd5\xe5\x80\xbc0\xa9\x94\x850\xdb\xe0\x0bT\xaeI.6\xb0\xc83\x9f\xfa\xf3(\xd0\x0b\x85\xb4R]\xe1\x16^T)Dm-\xc5A\x9c_\x97Dz\x8b\xc0b\xe4\xcc2\x8b\xf2A | |||
|
57 | s> \r\n | |||
|
58 | received frame(size=64; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) | |||
|
59 | s> 0\r\n | |||
|
60 | s> \r\n | |||
|
61 | response: [[b'\x1dok\x91\xd4J\xab\xa6\xd5\xe5\x80\xbc0\xa9\x94\x850\xdb\xe0\x0b', b'\xaeI.6\xb0\xc83\x9f\xfa\xf3(\xd0\x0b\x85\xb4R]\xe1\x16^', b')Dm-\xc5A\x9c_\x97Dz\x8b\xc0b\xe4\xcc2\x8b\xf2A']] | |||
|
62 | ||||
|
63 | Requesting just the public heads works | |||
|
64 | ||||
|
65 | $ sendhttpv2peer << EOF | |||
|
66 | > command heads | |||
|
67 | > publiconly 1 | |||
|
68 | > EOF | |||
|
69 | creating http peer for wire protocol version 2 | |||
|
70 | sending heads command | |||
|
71 | s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n | |||
|
72 | s> Accept-Encoding: identity\r\n | |||
|
73 | s> accept: application/mercurial-exp-framing-0003\r\n | |||
|
74 | s> content-type: application/mercurial-exp-framing-0003\r\n | |||
|
75 | s> content-length: 39\r\n | |||
|
76 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |||
|
77 | s> user-agent: Mercurial debugwireproto\r\n | |||
|
78 | s> \r\n | |||
|
79 | s> \x1f\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1JpubliconlyA1DnameEheads | |||
|
80 | s> makefile('rb', None) | |||
|
81 | s> HTTP/1.1 200 OK\r\n | |||
|
82 | s> Server: testing stub value\r\n | |||
|
83 | s> Date: $HTTP_DATE$\r\n | |||
|
84 | s> Content-Type: application/mercurial-exp-framing-0003\r\n | |||
|
85 | s> Transfer-Encoding: chunked\r\n | |||
|
86 | s> \r\n | |||
|
87 | s> 1e\r\n | |||
|
88 | s> \x16\x00\x00\x01\x00\x02\x01F | |||
|
89 | s> \x81Tx\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc | |||
|
90 | s> \r\n | |||
|
91 | received frame(size=22; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) | |||
|
92 | s> 0\r\n | |||
|
93 | s> \r\n | |||
|
94 | response: [[b'x\xd2\xdc\xa46\xb2\xf5\xb1\x88\xac&~)\xb8\x1e\x07&m8\xfc']] | |||
|
95 | ||||
|
96 | $ cat error.log |
@@ -1649,3 +1649,40 response is zlib compressed. | |||||
1649 |
|
1649 | |||
1650 | The server may also respond with a generic error type, which contains a string |
|
1650 | The server may also respond with a generic error type, which contains a string | |
1651 | indicating the failure. |
|
1651 | indicating the failure. | |
|
1652 | ||||
|
1653 | Frame-Based Protocol Commands | |||
|
1654 | ============================= | |||
|
1655 | ||||
|
1656 | **Experimental and under active development** | |||
|
1657 | ||||
|
1658 | This section documents the wire protocol commands exposed to transports | |||
|
1659 | using the frame-based protocol. The set of commands exposed through | |||
|
1660 | these transports is distinct from the set of commands exposed to legacy | |||
|
1661 | transports. | |||
|
1662 | ||||
|
1663 | The frame-based protocol uses CBOR to encode command execution requests. | |||
|
1664 | All command arguments must be mapped to a specific or set of CBOR data | |||
|
1665 | types. | |||
|
1666 | ||||
|
1667 | The response to many commands is also CBOR. There is no common response | |||
|
1668 | format: each command defines its own response format. | |||
|
1669 | ||||
|
1670 | TODO require node type be specified, as N bytes of binary node value | |||
|
1671 | could be ambiguous once SHA-1 is replaced. | |||
|
1672 | ||||
|
1673 | heads | |||
|
1674 | ----- | |||
|
1675 | ||||
|
1676 | Obtain DAG heads in the repository. | |||
|
1677 | ||||
|
1678 | The command accepts the following arguments: | |||
|
1679 | ||||
|
1680 | publiconly (optional) | |||
|
1681 | (boolean) If set, operate on the DAG for public phase changesets only. | |||
|
1682 | Non-public (i.e. draft) phase DAG heads will not be returned. | |||
|
1683 | ||||
|
1684 | The response is a CBOR array of bytestrings defining changeset nodes | |||
|
1685 | of DAG heads. The array can be empty if the repository is empty or no | |||
|
1686 | changesets satisfied the request. | |||
|
1687 | ||||
|
1688 | TODO consider exposing phase of heads in response |
@@ -518,7 +518,15 def dispatch(repo, proto, command): | |||||
518 | func, spec = commandtable[command] |
|
518 | func, spec = commandtable[command] | |
519 |
|
519 | |||
520 | args = proto.getargs(spec) |
|
520 | args = proto.getargs(spec) | |
|
521 | ||||
|
522 | # Version 1 protocols define arguments as a list. Version 2 uses a dict. | |||
|
523 | if isinstance(args, list): | |||
521 | return func(repo, proto, *args) |
|
524 | return func(repo, proto, *args) | |
|
525 | elif isinstance(args, dict): | |||
|
526 | return func(repo, proto, **args) | |||
|
527 | else: | |||
|
528 | raise error.ProgrammingError('unexpected type returned from ' | |||
|
529 | 'proto.getargs(): %s' % type(args)) | |||
522 |
|
530 | |||
523 | def options(cmd, keys, others): |
|
531 | def options(cmd, keys, others): | |
524 | opts = {} |
|
532 | opts = {} | |
@@ -996,7 +1004,7 def getbundle(repo, proto, others): | |||||
996 | return wireprototypes.streamres( |
|
1004 | return wireprototypes.streamres( | |
997 | gen=chunks, prefer_uncompressed=not prefercompressed) |
|
1005 | gen=chunks, prefer_uncompressed=not prefercompressed) | |
998 |
|
1006 | |||
999 | @wireprotocommand('heads', permission='pull') |
|
1007 | @wireprotocommand('heads', permission='pull', transportpolicy=POLICY_V1_ONLY) | |
1000 | def heads(repo, proto): |
|
1008 | def heads(repo, proto): | |
1001 | h = repo.heads() |
|
1009 | h = repo.heads() | |
1002 | return wireprototypes.bytesresponse(encodelist(h) + '\n') |
|
1010 | return wireprototypes.bytesresponse(encodelist(h) + '\n') | |
@@ -1197,3 +1205,13 def unbundle(repo, proto, heads): | |||||
1197 | bundler.newpart('error:pushraced', |
|
1205 | bundler.newpart('error:pushraced', | |
1198 | [('message', stringutil.forcebytestr(exc))]) |
|
1206 | [('message', stringutil.forcebytestr(exc))]) | |
1199 | return wireprototypes.streamreslegacy(gen=bundler.getchunks()) |
|
1207 | return wireprototypes.streamreslegacy(gen=bundler.getchunks()) | |
|
1208 | ||||
|
1209 | # Wire protocol version 2 commands only past this point. | |||
|
1210 | ||||
|
1211 | @wireprotocommand('heads', args='publiconly', permission='pull', | |||
|
1212 | transportpolicy=POLICY_V2_ONLY) | |||
|
1213 | def headsv2(repo, proto, publiconly=False): | |||
|
1214 | if publiconly: | |||
|
1215 | repo = repo.filtered('immutable') | |||
|
1216 | ||||
|
1217 | return wireprototypes.cborresponse(repo.heads()) |
@@ -349,7 +349,7 def createcommandframes(stream, requesti | |||||
349 | if done: |
|
349 | if done: | |
350 | break |
|
350 | break | |
351 |
|
351 | |||
352 | def createbytesresponseframesfrombytes(stream, requestid, data, |
|
352 | def createbytesresponseframesfrombytes(stream, requestid, data, iscbor=False, | |
353 | maxframesize=DEFAULT_MAX_FRAME_SIZE): |
|
353 | maxframesize=DEFAULT_MAX_FRAME_SIZE): | |
354 | """Create a raw frame to send a bytes response from static bytes input. |
|
354 | """Create a raw frame to send a bytes response from static bytes input. | |
355 |
|
355 | |||
@@ -358,9 +358,13 def createbytesresponseframesfrombytes(s | |||||
358 |
|
358 | |||
359 | # Simple case of a single frame. |
|
359 | # Simple case of a single frame. | |
360 | if len(data) <= maxframesize: |
|
360 | if len(data) <= maxframesize: | |
|
361 | flags = FLAG_BYTES_RESPONSE_EOS | |||
|
362 | if iscbor: | |||
|
363 | flags |= FLAG_BYTES_RESPONSE_CBOR | |||
|
364 | ||||
361 | yield stream.makeframe(requestid=requestid, |
|
365 | yield stream.makeframe(requestid=requestid, | |
362 | typeid=FRAME_TYPE_BYTES_RESPONSE, |
|
366 | typeid=FRAME_TYPE_BYTES_RESPONSE, | |
363 |
flags= |
|
367 | flags=flags, | |
364 | payload=data) |
|
368 | payload=data) | |
365 | return |
|
369 | return | |
366 |
|
370 | |||
@@ -375,6 +379,9 def createbytesresponseframesfrombytes(s | |||||
375 | else: |
|
379 | else: | |
376 | flags = FLAG_BYTES_RESPONSE_CONTINUATION |
|
380 | flags = FLAG_BYTES_RESPONSE_CONTINUATION | |
377 |
|
381 | |||
|
382 | if iscbor: | |||
|
383 | flags |= FLAG_BYTES_RESPONSE_CBOR | |||
|
384 | ||||
378 | yield stream.makeframe(requestid=requestid, |
|
385 | yield stream.makeframe(requestid=requestid, | |
379 | typeid=FRAME_TYPE_BYTES_RESPONSE, |
|
386 | typeid=FRAME_TYPE_BYTES_RESPONSE, | |
380 | flags=flags, |
|
387 | flags=flags, | |
@@ -608,7 +615,7 class serverreactor(object): | |||||
608 |
|
615 | |||
609 | return meth(frame) |
|
616 | return meth(frame) | |
610 |
|
617 | |||
611 | def onbytesresponseready(self, stream, requestid, data): |
|
618 | def onbytesresponseready(self, stream, requestid, data, iscbor=False): | |
612 | """Signal that a bytes response is ready to be sent to the client. |
|
619 | """Signal that a bytes response is ready to be sent to the client. | |
613 |
|
620 | |||
614 | The raw bytes response is passed as an argument. |
|
621 | The raw bytes response is passed as an argument. | |
@@ -617,7 +624,8 class serverreactor(object): | |||||
617 |
|
624 | |||
618 | def sendframes(): |
|
625 | def sendframes(): | |
619 | for frame in createbytesresponseframesfrombytes(stream, requestid, |
|
626 | for frame in createbytesresponseframesfrombytes(stream, requestid, | |
620 |
data |
|
627 | data, | |
|
628 | iscbor=iscbor): | |||
621 | yield frame |
|
629 | yield frame | |
622 |
|
630 | |||
623 | self._activecommands.remove(requestid) |
|
631 | self._activecommands.remove(requestid) |
@@ -12,6 +12,9 import sys | |||||
12 | import threading |
|
12 | import threading | |
13 |
|
13 | |||
14 | from .i18n import _ |
|
14 | from .i18n import _ | |
|
15 | from .thirdparty import ( | |||
|
16 | cbor, | |||
|
17 | ) | |||
15 | from .thirdparty.zope import ( |
|
18 | from .thirdparty.zope import ( | |
16 | interface as zi, |
|
19 | interface as zi, | |
17 | ) |
|
20 | ) | |
@@ -563,6 +566,12 def _httpv2runcommand(ui, repo, req, res | |||||
563 | action, meta = reactor.onbytesresponseready(outstream, |
|
566 | action, meta = reactor.onbytesresponseready(outstream, | |
564 | command['requestid'], |
|
567 | command['requestid'], | |
565 | rsp.data) |
|
568 | rsp.data) | |
|
569 | elif isinstance(rsp, wireprototypes.cborresponse): | |||
|
570 | encoded = cbor.dumps(rsp.value, canonical=True) | |||
|
571 | action, meta = reactor.onbytesresponseready(outstream, | |||
|
572 | command['requestid'], | |||
|
573 | encoded, | |||
|
574 | iscbor=True) | |||
566 | else: |
|
575 | else: | |
567 | action, meta = reactor.onapplicationerror( |
|
576 | action, meta = reactor.onapplicationerror( | |
568 | _('unhandled response type from wire proto command')) |
|
577 | _('unhandled response type from wire proto command')) | |
@@ -600,10 +609,10 class httpv2protocolhandler(object): | |||||
600 | for k in args.split(): |
|
609 | for k in args.split(): | |
601 | if k == '*': |
|
610 | if k == '*': | |
602 | raise NotImplementedError('do not support * args') |
|
611 | raise NotImplementedError('do not support * args') | |
603 | else: |
|
612 | elif k in self._args: | |
604 | data[k] = self._args[k] |
|
613 | data[k] = self._args[k] | |
605 |
|
614 | |||
606 | return [data[k] for k in args.split()] |
|
615 | return data | |
607 |
|
616 | |||
608 | def getprotocaps(self): |
|
617 | def getprotocaps(self): | |
609 | # Protocol capabilities are currently not implemented for HTTP V2. |
|
618 | # Protocol capabilities are currently not implemented for HTTP V2. |
@@ -97,6 +97,11 class streamreslegacy(object): | |||||
97 | def __init__(self, gen=None): |
|
97 | def __init__(self, gen=None): | |
98 | self.gen = gen |
|
98 | self.gen = gen | |
99 |
|
99 | |||
|
100 | class cborresponse(object): | |||
|
101 | """Encode the response value as CBOR.""" | |||
|
102 | def __init__(self, v): | |||
|
103 | self.value = v | |||
|
104 | ||||
100 | class baseprotocolhandler(zi.Interface): |
|
105 | class baseprotocolhandler(zi.Interface): | |
101 | """Abstract base class for wire protocol handlers. |
|
106 | """Abstract base class for wire protocol handlers. | |
102 |
|
107 | |||
@@ -115,7 +120,10 class baseprotocolhandler(zi.Interface): | |||||
115 | def getargs(args): |
|
120 | def getargs(args): | |
116 | """return the value for arguments in <args> |
|
121 | """return the value for arguments in <args> | |
117 |
|
122 | |||
118 | returns a list of values (same order as <args>)""" |
|
123 | For version 1 transports, returns a list of values in the same | |
|
124 | order they appear in ``args``. For version 2 transports, returns | |||
|
125 | a dict mapping argument name to value. | |||
|
126 | """ | |||
119 |
|
127 | |||
120 | def getprotocaps(): |
|
128 | def getprotocaps(): | |
121 | """Returns the list of protocol-level capabilities of client |
|
129 | """Returns the list of protocol-level capabilities of client |
General Comments 0
You need to be logged in to leave comments.
Login now