##// END OF EJS Templates
wireprotov2: advertise redirect targets in capabilities...
Gregory Szorc -
r40059:10cf8b11 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (601 lines changed) Show them Hide them
@@ -0,0 +1,601
1 $ . $TESTDIR/wireprotohelpers.sh
2
3 $ hg init server
4 $ enablehttpv2 server
5 $ cd server
6 $ cat >> .hg/hgrc << EOF
7 > [extensions]
8 > simplecache = $TESTDIR/wireprotosimplecache.py
9 > EOF
10
11 $ echo a0 > a
12 $ echo b0 > b
13 $ hg -q commit -A -m 'commit 0'
14 $ echo a1 > a
15 $ hg commit -m 'commit 1'
16
17 $ hg --debug debugindex -m
18 rev linkrev nodeid p1 p2
19 0 0 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000
20 1 1 a988fb43583e871d1ed5750ee074c6d840bbbfc8 992f4779029a3df8d0666d00bb924f69634e2641 0000000000000000000000000000000000000000
21
22 $ hg --config simplecache.redirectsfile=redirects.py serve -p $HGPORT -d --pid-file hg.pid -E error.log
23 $ cat hg.pid > $DAEMON_PIDS
24
25 $ cat > redirects.py << EOF
26 > [
27 > {
28 > b'name': b'target-a',
29 > b'protocol': b'http',
30 > b'snirequired': False,
31 > b'tlsversions': [b'1.2', b'1.3'],
32 > b'uris': [b'http://example.com/'],
33 > },
34 > ]
35 > EOF
36
37 Redirect targets advertised when configured
38
39 $ sendhttpv2peerhandshake << EOF
40 > command capabilities
41 > EOF
42 creating http peer for wire protocol version 2
43 s> GET /?cmd=capabilities HTTP/1.1\r\n
44 s> Accept-Encoding: identity\r\n
45 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
46 s> x-hgproto-1: cbor\r\n
47 s> x-hgupgrade-1: exp-http-v2-0002\r\n
48 s> accept: application/mercurial-0.1\r\n
49 s> host: $LOCALIP:$HGPORT\r\n (glob)
50 s> user-agent: Mercurial debugwireproto\r\n
51 s> \r\n
52 s> makefile('rb', None)
53 s> HTTP/1.1 200 OK\r\n
54 s> Server: testing stub value\r\n
55 s> Date: $HTTP_DATE$\r\n
56 s> Content-Type: application/mercurial-cbor\r\n
57 s> Content-Length: 1970\r\n
58 s> \r\n
59 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa5DnameHtarget-aHprotocolDhttpKsnirequired\xf4Ktlsversions\x82C1.2C1.3Duris\x81Shttp://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
60 sending capabilities command
61 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
62 s> Accept-Encoding: identity\r\n
63 s> accept: application/mercurial-exp-framing-0005\r\n
64 s> content-type: application/mercurial-exp-framing-0005\r\n
65 s> content-length: 27\r\n
66 s> host: $LOCALIP:$HGPORT\r\n (glob)
67 s> user-agent: Mercurial debugwireproto\r\n
68 s> \r\n
69 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
70 s> makefile('rb', None)
71 s> HTTP/1.1 200 OK\r\n
72 s> Server: testing stub value\r\n
73 s> Date: $HTTP_DATE$\r\n
74 s> Content-Type: application/mercurial-exp-framing-0005\r\n
75 s> Transfer-Encoding: chunked\r\n
76 s> \r\n
77 s> 13\r\n
78 s> \x0b\x00\x00\x01\x00\x02\x011
79 s> \xa1FstatusBok
80 s> \r\n
81 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
82 s> 5ab\r\n
83 s> \xa3\x05\x00\x01\x00\x02\x001
84 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x81\xa5DnameHtarget-aHprotocolDhttpKsnirequired\xf4Ktlsversions\x82C1.2C1.3Duris\x81Shttp://example.com/
85 s> \r\n
86 received frame(size=1443; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
87 s> 8\r\n
88 s> \x00\x00\x00\x01\x00\x02\x002
89 s> \r\n
90 s> 0\r\n
91 s> \r\n
92 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
93 response: gen[
94 {
95 b'commands': {
96 b'branchmap': {
97 b'args': {},
98 b'permissions': [
99 b'pull'
100 ]
101 },
102 b'capabilities': {
103 b'args': {},
104 b'permissions': [
105 b'pull'
106 ]
107 },
108 b'changesetdata': {
109 b'args': {
110 b'fields': {
111 b'default': set([]),
112 b'required': False,
113 b'type': b'set',
114 b'validvalues': set([
115 b'bookmarks',
116 b'parents',
117 b'phase',
118 b'revision'
119 ])
120 },
121 b'noderange': {
122 b'default': None,
123 b'required': False,
124 b'type': b'list'
125 },
126 b'nodes': {
127 b'default': None,
128 b'required': False,
129 b'type': b'list'
130 },
131 b'nodesdepth': {
132 b'default': None,
133 b'required': False,
134 b'type': b'int'
135 }
136 },
137 b'permissions': [
138 b'pull'
139 ]
140 },
141 b'filedata': {
142 b'args': {
143 b'fields': {
144 b'default': set([]),
145 b'required': False,
146 b'type': b'set',
147 b'validvalues': set([
148 b'parents',
149 b'revision'
150 ])
151 },
152 b'haveparents': {
153 b'default': False,
154 b'required': False,
155 b'type': b'bool'
156 },
157 b'nodes': {
158 b'required': True,
159 b'type': b'list'
160 },
161 b'path': {
162 b'required': True,
163 b'type': b'bytes'
164 }
165 },
166 b'permissions': [
167 b'pull'
168 ]
169 },
170 b'heads': {
171 b'args': {
172 b'publiconly': {
173 b'default': False,
174 b'required': False,
175 b'type': b'bool'
176 }
177 },
178 b'permissions': [
179 b'pull'
180 ]
181 },
182 b'known': {
183 b'args': {
184 b'nodes': {
185 b'default': [],
186 b'required': False,
187 b'type': b'list'
188 }
189 },
190 b'permissions': [
191 b'pull'
192 ]
193 },
194 b'listkeys': {
195 b'args': {
196 b'namespace': {
197 b'required': True,
198 b'type': b'bytes'
199 }
200 },
201 b'permissions': [
202 b'pull'
203 ]
204 },
205 b'lookup': {
206 b'args': {
207 b'key': {
208 b'required': True,
209 b'type': b'bytes'
210 }
211 },
212 b'permissions': [
213 b'pull'
214 ]
215 },
216 b'manifestdata': {
217 b'args': {
218 b'fields': {
219 b'default': set([]),
220 b'required': False,
221 b'type': b'set',
222 b'validvalues': set([
223 b'parents',
224 b'revision'
225 ])
226 },
227 b'haveparents': {
228 b'default': False,
229 b'required': False,
230 b'type': b'bool'
231 },
232 b'nodes': {
233 b'required': True,
234 b'type': b'list'
235 },
236 b'tree': {
237 b'required': True,
238 b'type': b'bytes'
239 }
240 },
241 b'permissions': [
242 b'pull'
243 ]
244 },
245 b'pushkey': {
246 b'args': {
247 b'key': {
248 b'required': True,
249 b'type': b'bytes'
250 },
251 b'namespace': {
252 b'required': True,
253 b'type': b'bytes'
254 },
255 b'new': {
256 b'required': True,
257 b'type': b'bytes'
258 },
259 b'old': {
260 b'required': True,
261 b'type': b'bytes'
262 }
263 },
264 b'permissions': [
265 b'push'
266 ]
267 }
268 },
269 b'compression': [
270 {
271 b'name': b'zstd'
272 },
273 {
274 b'name': b'zlib'
275 }
276 ],
277 b'framingmediatypes': [
278 b'application/mercurial-exp-framing-0005'
279 ],
280 b'pathfilterprefixes': set([
281 b'path:',
282 b'rootfilesin:'
283 ]),
284 b'rawrepoformats': [
285 b'generaldelta',
286 b'revlogv1'
287 ],
288 b'redirect': {
289 b'hashes': [
290 b'sha256',
291 b'sha1'
292 ],
293 b'targets': [
294 {
295 b'name': b'target-a',
296 b'protocol': b'http',
297 b'snirequired': False,
298 b'tlsversions': [
299 b'1.2',
300 b'1.3'
301 ],
302 b'uris': [
303 b'http://example.com/'
304 ]
305 }
306 ]
307 }
308 }
309 ]
310
311 $ cat > redirects.py << EOF
312 > [
313 > {
314 > b'name': b'target-a',
315 > b'protocol': b'http',
316 > b'uris': [b'http://example.com/'],
317 > },
318 > {
319 > b'name': b'target-b',
320 > b'protocol': b'unknown',
321 > b'uris': [b'unknown://example.com/'],
322 > },
323 > ]
324 > EOF
325
326 $ sendhttpv2peerhandshake << EOF
327 > command capabilities
328 > EOF
329 creating http peer for wire protocol version 2
330 s> GET /?cmd=capabilities HTTP/1.1\r\n
331 s> Accept-Encoding: identity\r\n
332 s> vary: X-HgProto-1,X-HgUpgrade-1\r\n
333 s> x-hgproto-1: cbor\r\n
334 s> x-hgupgrade-1: exp-http-v2-0002\r\n
335 s> accept: application/mercurial-0.1\r\n
336 s> host: $LOCALIP:$HGPORT\r\n (glob)
337 s> user-agent: Mercurial debugwireproto\r\n
338 s> \r\n
339 s> makefile('rb', None)
340 s> HTTP/1.1 200 OK\r\n
341 s> Server: testing stub value\r\n
342 s> Date: $HTTP_DATE$\r\n
343 s> Content-Type: application/mercurial-cbor\r\n
344 s> Content-Length: 1997\r\n
345 s> \r\n
346 s> \xa3GapibaseDapi/Dapis\xa1Pexp-http-v2-0002\xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x82\xa3DnameHtarget-aHprotocolDhttpDuris\x81Shttp://example.com/\xa3DnameHtarget-bHprotocolGunknownDuris\x81Vunknown://example.com/Nv1capabilitiesY\x01\xd8batch branchmap $USUAL_BUNDLE2_CAPS$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash
347 sending capabilities command
348 s> POST /api/exp-http-v2-0002/ro/capabilities HTTP/1.1\r\n
349 s> Accept-Encoding: identity\r\n
350 s> accept: application/mercurial-exp-framing-0005\r\n
351 s> content-type: application/mercurial-exp-framing-0005\r\n
352 s> content-length: 27\r\n
353 s> host: $LOCALIP:$HGPORT\r\n (glob)
354 s> user-agent: Mercurial debugwireproto\r\n
355 s> \r\n
356 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
357 s> makefile('rb', None)
358 s> HTTP/1.1 200 OK\r\n
359 s> Server: testing stub value\r\n
360 s> Date: $HTTP_DATE$\r\n
361 s> Content-Type: application/mercurial-exp-framing-0005\r\n
362 s> Transfer-Encoding: chunked\r\n
363 s> \r\n
364 s> 13\r\n
365 s> \x0b\x00\x00\x01\x00\x02\x011
366 s> \xa1FstatusBok
367 s> \r\n
368 received frame(size=11; request=1; stream=2; streamflags=stream-begin; type=command-response; flags=continuation)
369 s> 5c6\r\n
370 s> \xbe\x05\x00\x01\x00\x02\x001
371 s> \xa6Hcommands\xaaIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullMchangesetdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x84IbookmarksGparentsEphaseHrevisionInoderange\xa3Gdefault\xf6Hrequired\xf4DtypeDlistEnodes\xa3Gdefault\xf6Hrequired\xf4DtypeDlistJnodesdepth\xa3Gdefault\xf6Hrequired\xf4DtypeCintKpermissions\x81DpullHfiledata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDpath\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullEheads\xa2Dargs\xa1Jpubliconly\xa3Gdefault\xf4Hrequired\xf4DtypeDboolKpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\xa3Gdefault\x80Hrequired\xf4DtypeDlistKpermissions\x81DpullHlistkeys\xa2Dargs\xa1Inamespace\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullFlookup\xa2Dargs\xa1Ckey\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullLmanifestdata\xa2Dargs\xa4Ffields\xa4Gdefault\xd9\x01\x02\x80Hrequired\xf4DtypeCsetKvalidvalues\xd9\x01\x02\x82GparentsHrevisionKhaveparents\xa3Gdefault\xf4Hrequired\xf4DtypeDboolEnodes\xa2Hrequired\xf5DtypeDlistDtree\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpullGpushkey\xa2Dargs\xa4Ckey\xa2Hrequired\xf5DtypeEbytesInamespace\xa2Hrequired\xf5DtypeEbytesCnew\xa2Hrequired\xf5DtypeEbytesCold\xa2Hrequired\xf5DtypeEbytesKpermissions\x81DpushKcompression\x82\xa1DnameDzstd\xa1DnameDzlibQframingmediatypes\x81X&application/mercurial-exp-framing-0005Rpathfilterprefixes\xd9\x01\x02\x82Epath:Lrootfilesin:Nrawrepoformats\x82LgeneraldeltaHrevlogv1Hredirect\xa2Fhashes\x82Fsha256Dsha1Gtargets\x82\xa3DnameHtarget-aHprotocolDhttpDuris\x81Shttp://example.com/\xa3DnameHtarget-bHprotocolGunknownDuris\x81Vunknown://example.com/
372 s> \r\n
373 received frame(size=1470; request=1; stream=2; streamflags=; type=command-response; flags=continuation)
374 s> 8\r\n
375 s> \x00\x00\x00\x01\x00\x02\x002
376 s> \r\n
377 s> 0\r\n
378 s> \r\n
379 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
380 response: gen[
381 {
382 b'commands': {
383 b'branchmap': {
384 b'args': {},
385 b'permissions': [
386 b'pull'
387 ]
388 },
389 b'capabilities': {
390 b'args': {},
391 b'permissions': [
392 b'pull'
393 ]
394 },
395 b'changesetdata': {
396 b'args': {
397 b'fields': {
398 b'default': set([]),
399 b'required': False,
400 b'type': b'set',
401 b'validvalues': set([
402 b'bookmarks',
403 b'parents',
404 b'phase',
405 b'revision'
406 ])
407 },
408 b'noderange': {
409 b'default': None,
410 b'required': False,
411 b'type': b'list'
412 },
413 b'nodes': {
414 b'default': None,
415 b'required': False,
416 b'type': b'list'
417 },
418 b'nodesdepth': {
419 b'default': None,
420 b'required': False,
421 b'type': b'int'
422 }
423 },
424 b'permissions': [
425 b'pull'
426 ]
427 },
428 b'filedata': {
429 b'args': {
430 b'fields': {
431 b'default': set([]),
432 b'required': False,
433 b'type': b'set',
434 b'validvalues': set([
435 b'parents',
436 b'revision'
437 ])
438 },
439 b'haveparents': {
440 b'default': False,
441 b'required': False,
442 b'type': b'bool'
443 },
444 b'nodes': {
445 b'required': True,
446 b'type': b'list'
447 },
448 b'path': {
449 b'required': True,
450 b'type': b'bytes'
451 }
452 },
453 b'permissions': [
454 b'pull'
455 ]
456 },
457 b'heads': {
458 b'args': {
459 b'publiconly': {
460 b'default': False,
461 b'required': False,
462 b'type': b'bool'
463 }
464 },
465 b'permissions': [
466 b'pull'
467 ]
468 },
469 b'known': {
470 b'args': {
471 b'nodes': {
472 b'default': [],
473 b'required': False,
474 b'type': b'list'
475 }
476 },
477 b'permissions': [
478 b'pull'
479 ]
480 },
481 b'listkeys': {
482 b'args': {
483 b'namespace': {
484 b'required': True,
485 b'type': b'bytes'
486 }
487 },
488 b'permissions': [
489 b'pull'
490 ]
491 },
492 b'lookup': {
493 b'args': {
494 b'key': {
495 b'required': True,
496 b'type': b'bytes'
497 }
498 },
499 b'permissions': [
500 b'pull'
501 ]
502 },
503 b'manifestdata': {
504 b'args': {
505 b'fields': {
506 b'default': set([]),
507 b'required': False,
508 b'type': b'set',
509 b'validvalues': set([
510 b'parents',
511 b'revision'
512 ])
513 },
514 b'haveparents': {
515 b'default': False,
516 b'required': False,
517 b'type': b'bool'
518 },
519 b'nodes': {
520 b'required': True,
521 b'type': b'list'
522 },
523 b'tree': {
524 b'required': True,
525 b'type': b'bytes'
526 }
527 },
528 b'permissions': [
529 b'pull'
530 ]
531 },
532 b'pushkey': {
533 b'args': {
534 b'key': {
535 b'required': True,
536 b'type': b'bytes'
537 },
538 b'namespace': {
539 b'required': True,
540 b'type': b'bytes'
541 },
542 b'new': {
543 b'required': True,
544 b'type': b'bytes'
545 },
546 b'old': {
547 b'required': True,
548 b'type': b'bytes'
549 }
550 },
551 b'permissions': [
552 b'push'
553 ]
554 }
555 },
556 b'compression': [
557 {
558 b'name': b'zstd'
559 },
560 {
561 b'name': b'zlib'
562 }
563 ],
564 b'framingmediatypes': [
565 b'application/mercurial-exp-framing-0005'
566 ],
567 b'pathfilterprefixes': set([
568 b'path:',
569 b'rootfilesin:'
570 ]),
571 b'rawrepoformats': [
572 b'generaldelta',
573 b'revlogv1'
574 ],
575 b'redirect': {
576 b'hashes': [
577 b'sha256',
578 b'sha1'
579 ],
580 b'targets': [
581 {
582 b'name': b'target-a',
583 b'protocol': b'http',
584 b'uris': [
585 b'http://example.com/'
586 ]
587 },
588 {
589 b'name': b'target-b',
590 b'protocol': b'unknown',
591 b'uris': [
592 b'unknown://example.com/'
593 ]
594 }
595 ]
596 }
597 }
598 ]
599
600 $ cat error.log
601 $ killdaemons.py
@@ -1,1129 +1,1187
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10 import hashlib
10 import hashlib
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 hex,
14 hex,
15 nullid,
15 nullid,
16 )
16 )
17 from . import (
17 from . import (
18 discovery,
18 discovery,
19 encoding,
19 encoding,
20 error,
20 error,
21 narrowspec,
21 narrowspec,
22 pycompat,
22 pycompat,
23 streamclone,
23 streamclone,
24 util,
24 util,
25 wireprotoframing,
25 wireprotoframing,
26 wireprototypes,
26 wireprototypes,
27 )
27 )
28 from .utils import (
28 from .utils import (
29 cborutil,
29 cborutil,
30 interfaceutil,
30 interfaceutil,
31 stringutil,
31 stringutil,
32 )
32 )
33
33
34 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
34 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
35
35
36 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
36 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
37
37
38 COMMANDS = wireprototypes.commanddict()
38 COMMANDS = wireprototypes.commanddict()
39
39
40 # Value inserted into cache key computation function. Change the value to
40 # Value inserted into cache key computation function. Change the value to
41 # force new cache keys for every command request. This should be done when
41 # force new cache keys for every command request. This should be done when
42 # there is a change to how caching works, etc.
42 # there is a change to how caching works, etc.
43 GLOBAL_CACHE_VERSION = 1
43 GLOBAL_CACHE_VERSION = 1
44
44
45 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
45 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
46 from .hgweb import common as hgwebcommon
46 from .hgweb import common as hgwebcommon
47
47
48 # URL space looks like: <permissions>/<command>, where <permission> can
48 # URL space looks like: <permissions>/<command>, where <permission> can
49 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
49 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
50
50
51 # Root URL does nothing meaningful... yet.
51 # Root URL does nothing meaningful... yet.
52 if not urlparts:
52 if not urlparts:
53 res.status = b'200 OK'
53 res.status = b'200 OK'
54 res.headers[b'Content-Type'] = b'text/plain'
54 res.headers[b'Content-Type'] = b'text/plain'
55 res.setbodybytes(_('HTTP version 2 API handler'))
55 res.setbodybytes(_('HTTP version 2 API handler'))
56 return
56 return
57
57
58 if len(urlparts) == 1:
58 if len(urlparts) == 1:
59 res.status = b'404 Not Found'
59 res.status = b'404 Not Found'
60 res.headers[b'Content-Type'] = b'text/plain'
60 res.headers[b'Content-Type'] = b'text/plain'
61 res.setbodybytes(_('do not know how to process %s\n') %
61 res.setbodybytes(_('do not know how to process %s\n') %
62 req.dispatchpath)
62 req.dispatchpath)
63 return
63 return
64
64
65 permission, command = urlparts[0:2]
65 permission, command = urlparts[0:2]
66
66
67 if permission not in (b'ro', b'rw'):
67 if permission not in (b'ro', b'rw'):
68 res.status = b'404 Not Found'
68 res.status = b'404 Not Found'
69 res.headers[b'Content-Type'] = b'text/plain'
69 res.headers[b'Content-Type'] = b'text/plain'
70 res.setbodybytes(_('unknown permission: %s') % permission)
70 res.setbodybytes(_('unknown permission: %s') % permission)
71 return
71 return
72
72
73 if req.method != 'POST':
73 if req.method != 'POST':
74 res.status = b'405 Method Not Allowed'
74 res.status = b'405 Method Not Allowed'
75 res.headers[b'Allow'] = b'POST'
75 res.headers[b'Allow'] = b'POST'
76 res.setbodybytes(_('commands require POST requests'))
76 res.setbodybytes(_('commands require POST requests'))
77 return
77 return
78
78
79 # At some point we'll want to use our own API instead of recycling the
79 # At some point we'll want to use our own API instead of recycling the
80 # behavior of version 1 of the wire protocol...
80 # behavior of version 1 of the wire protocol...
81 # TODO return reasonable responses - not responses that overload the
81 # TODO return reasonable responses - not responses that overload the
82 # HTTP status line message for error reporting.
82 # HTTP status line message for error reporting.
83 try:
83 try:
84 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
84 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
85 except hgwebcommon.ErrorResponse as e:
85 except hgwebcommon.ErrorResponse as e:
86 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
86 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
87 for k, v in e.headers:
87 for k, v in e.headers:
88 res.headers[k] = v
88 res.headers[k] = v
89 res.setbodybytes('permission denied')
89 res.setbodybytes('permission denied')
90 return
90 return
91
91
92 # We have a special endpoint to reflect the request back at the client.
92 # We have a special endpoint to reflect the request back at the client.
93 if command == b'debugreflect':
93 if command == b'debugreflect':
94 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
94 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
95 return
95 return
96
96
97 # Extra commands that we handle that aren't really wire protocol
97 # Extra commands that we handle that aren't really wire protocol
98 # commands. Think extra hard before making this hackery available to
98 # commands. Think extra hard before making this hackery available to
99 # extension.
99 # extension.
100 extracommands = {'multirequest'}
100 extracommands = {'multirequest'}
101
101
102 if command not in COMMANDS and command not in extracommands:
102 if command not in COMMANDS and command not in extracommands:
103 res.status = b'404 Not Found'
103 res.status = b'404 Not Found'
104 res.headers[b'Content-Type'] = b'text/plain'
104 res.headers[b'Content-Type'] = b'text/plain'
105 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
105 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
106 return
106 return
107
107
108 repo = rctx.repo
108 repo = rctx.repo
109 ui = repo.ui
109 ui = repo.ui
110
110
111 proto = httpv2protocolhandler(req, ui)
111 proto = httpv2protocolhandler(req, ui)
112
112
113 if (not COMMANDS.commandavailable(command, proto)
113 if (not COMMANDS.commandavailable(command, proto)
114 and command not in extracommands):
114 and command not in extracommands):
115 res.status = b'404 Not Found'
115 res.status = b'404 Not Found'
116 res.headers[b'Content-Type'] = b'text/plain'
116 res.headers[b'Content-Type'] = b'text/plain'
117 res.setbodybytes(_('invalid wire protocol command: %s') % command)
117 res.setbodybytes(_('invalid wire protocol command: %s') % command)
118 return
118 return
119
119
120 # TODO consider cases where proxies may add additional Accept headers.
120 # TODO consider cases where proxies may add additional Accept headers.
121 if req.headers.get(b'Accept') != FRAMINGTYPE:
121 if req.headers.get(b'Accept') != FRAMINGTYPE:
122 res.status = b'406 Not Acceptable'
122 res.status = b'406 Not Acceptable'
123 res.headers[b'Content-Type'] = b'text/plain'
123 res.headers[b'Content-Type'] = b'text/plain'
124 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
124 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
125 % FRAMINGTYPE)
125 % FRAMINGTYPE)
126 return
126 return
127
127
128 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
128 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
129 res.status = b'415 Unsupported Media Type'
129 res.status = b'415 Unsupported Media Type'
130 # TODO we should send a response with appropriate media type,
130 # TODO we should send a response with appropriate media type,
131 # since client does Accept it.
131 # since client does Accept it.
132 res.headers[b'Content-Type'] = b'text/plain'
132 res.headers[b'Content-Type'] = b'text/plain'
133 res.setbodybytes(_('client MUST send Content-Type header with '
133 res.setbodybytes(_('client MUST send Content-Type header with '
134 'value: %s\n') % FRAMINGTYPE)
134 'value: %s\n') % FRAMINGTYPE)
135 return
135 return
136
136
137 _processhttpv2request(ui, repo, req, res, permission, command, proto)
137 _processhttpv2request(ui, repo, req, res, permission, command, proto)
138
138
139 def _processhttpv2reflectrequest(ui, repo, req, res):
139 def _processhttpv2reflectrequest(ui, repo, req, res):
140 """Reads unified frame protocol request and dumps out state to client.
140 """Reads unified frame protocol request and dumps out state to client.
141
141
142 This special endpoint can be used to help debug the wire protocol.
142 This special endpoint can be used to help debug the wire protocol.
143
143
144 Instead of routing the request through the normal dispatch mechanism,
144 Instead of routing the request through the normal dispatch mechanism,
145 we instead read all frames, decode them, and feed them into our state
145 we instead read all frames, decode them, and feed them into our state
146 tracker. We then dump the log of all that activity back out to the
146 tracker. We then dump the log of all that activity back out to the
147 client.
147 client.
148 """
148 """
149 import json
149 import json
150
150
151 # Reflection APIs have a history of being abused, accidentally disclosing
151 # Reflection APIs have a history of being abused, accidentally disclosing
152 # sensitive data, etc. So we have a config knob.
152 # sensitive data, etc. So we have a config knob.
153 if not ui.configbool('experimental', 'web.api.debugreflect'):
153 if not ui.configbool('experimental', 'web.api.debugreflect'):
154 res.status = b'404 Not Found'
154 res.status = b'404 Not Found'
155 res.headers[b'Content-Type'] = b'text/plain'
155 res.headers[b'Content-Type'] = b'text/plain'
156 res.setbodybytes(_('debugreflect service not available'))
156 res.setbodybytes(_('debugreflect service not available'))
157 return
157 return
158
158
159 # We assume we have a unified framing protocol request body.
159 # We assume we have a unified framing protocol request body.
160
160
161 reactor = wireprotoframing.serverreactor()
161 reactor = wireprotoframing.serverreactor()
162 states = []
162 states = []
163
163
164 while True:
164 while True:
165 frame = wireprotoframing.readframe(req.bodyfh)
165 frame = wireprotoframing.readframe(req.bodyfh)
166
166
167 if not frame:
167 if not frame:
168 states.append(b'received: <no frame>')
168 states.append(b'received: <no frame>')
169 break
169 break
170
170
171 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
171 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
172 frame.requestid,
172 frame.requestid,
173 frame.payload))
173 frame.payload))
174
174
175 action, meta = reactor.onframerecv(frame)
175 action, meta = reactor.onframerecv(frame)
176 states.append(json.dumps((action, meta), sort_keys=True,
176 states.append(json.dumps((action, meta), sort_keys=True,
177 separators=(', ', ': ')))
177 separators=(', ', ': ')))
178
178
179 action, meta = reactor.oninputeof()
179 action, meta = reactor.oninputeof()
180 meta['action'] = action
180 meta['action'] = action
181 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
181 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
182
182
183 res.status = b'200 OK'
183 res.status = b'200 OK'
184 res.headers[b'Content-Type'] = b'text/plain'
184 res.headers[b'Content-Type'] = b'text/plain'
185 res.setbodybytes(b'\n'.join(states))
185 res.setbodybytes(b'\n'.join(states))
186
186
187 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
187 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
188 """Post-validation handler for HTTPv2 requests.
188 """Post-validation handler for HTTPv2 requests.
189
189
190 Called when the HTTP request contains unified frame-based protocol
190 Called when the HTTP request contains unified frame-based protocol
191 frames for evaluation.
191 frames for evaluation.
192 """
192 """
193 # TODO Some HTTP clients are full duplex and can receive data before
193 # TODO Some HTTP clients are full duplex and can receive data before
194 # the entire request is transmitted. Figure out a way to indicate support
194 # the entire request is transmitted. Figure out a way to indicate support
195 # for that so we can opt into full duplex mode.
195 # for that so we can opt into full duplex mode.
196 reactor = wireprotoframing.serverreactor(deferoutput=True)
196 reactor = wireprotoframing.serverreactor(deferoutput=True)
197 seencommand = False
197 seencommand = False
198
198
199 outstream = reactor.makeoutputstream()
199 outstream = reactor.makeoutputstream()
200
200
201 while True:
201 while True:
202 frame = wireprotoframing.readframe(req.bodyfh)
202 frame = wireprotoframing.readframe(req.bodyfh)
203 if not frame:
203 if not frame:
204 break
204 break
205
205
206 action, meta = reactor.onframerecv(frame)
206 action, meta = reactor.onframerecv(frame)
207
207
208 if action == 'wantframe':
208 if action == 'wantframe':
209 # Need more data before we can do anything.
209 # Need more data before we can do anything.
210 continue
210 continue
211 elif action == 'runcommand':
211 elif action == 'runcommand':
212 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
212 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
213 reqcommand, reactor, outstream,
213 reqcommand, reactor, outstream,
214 meta, issubsequent=seencommand)
214 meta, issubsequent=seencommand)
215
215
216 if sentoutput:
216 if sentoutput:
217 return
217 return
218
218
219 seencommand = True
219 seencommand = True
220
220
221 elif action == 'error':
221 elif action == 'error':
222 # TODO define proper error mechanism.
222 # TODO define proper error mechanism.
223 res.status = b'200 OK'
223 res.status = b'200 OK'
224 res.headers[b'Content-Type'] = b'text/plain'
224 res.headers[b'Content-Type'] = b'text/plain'
225 res.setbodybytes(meta['message'] + b'\n')
225 res.setbodybytes(meta['message'] + b'\n')
226 return
226 return
227 else:
227 else:
228 raise error.ProgrammingError(
228 raise error.ProgrammingError(
229 'unhandled action from frame processor: %s' % action)
229 'unhandled action from frame processor: %s' % action)
230
230
231 action, meta = reactor.oninputeof()
231 action, meta = reactor.oninputeof()
232 if action == 'sendframes':
232 if action == 'sendframes':
233 # We assume we haven't started sending the response yet. If we're
233 # We assume we haven't started sending the response yet. If we're
234 # wrong, the response type will raise an exception.
234 # wrong, the response type will raise an exception.
235 res.status = b'200 OK'
235 res.status = b'200 OK'
236 res.headers[b'Content-Type'] = FRAMINGTYPE
236 res.headers[b'Content-Type'] = FRAMINGTYPE
237 res.setbodygen(meta['framegen'])
237 res.setbodygen(meta['framegen'])
238 elif action == 'noop':
238 elif action == 'noop':
239 pass
239 pass
240 else:
240 else:
241 raise error.ProgrammingError('unhandled action from frame processor: %s'
241 raise error.ProgrammingError('unhandled action from frame processor: %s'
242 % action)
242 % action)
243
243
244 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
244 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
245 outstream, command, issubsequent):
245 outstream, command, issubsequent):
246 """Dispatch a wire protocol command made from HTTPv2 requests.
246 """Dispatch a wire protocol command made from HTTPv2 requests.
247
247
248 The authenticated permission (``authedperm``) along with the original
248 The authenticated permission (``authedperm``) along with the original
249 command from the URL (``reqcommand``) are passed in.
249 command from the URL (``reqcommand``) are passed in.
250 """
250 """
251 # We already validated that the session has permissions to perform the
251 # We already validated that the session has permissions to perform the
252 # actions in ``authedperm``. In the unified frame protocol, the canonical
252 # actions in ``authedperm``. In the unified frame protocol, the canonical
253 # command to run is expressed in a frame. However, the URL also requested
253 # command to run is expressed in a frame. However, the URL also requested
254 # to run a specific command. We need to be careful that the command we
254 # to run a specific command. We need to be careful that the command we
255 # run doesn't have permissions requirements greater than what was granted
255 # run doesn't have permissions requirements greater than what was granted
256 # by ``authedperm``.
256 # by ``authedperm``.
257 #
257 #
258 # Our rule for this is we only allow one command per HTTP request and
258 # Our rule for this is we only allow one command per HTTP request and
259 # that command must match the command in the URL. However, we make
259 # that command must match the command in the URL. However, we make
260 # an exception for the ``multirequest`` URL. This URL is allowed to
260 # an exception for the ``multirequest`` URL. This URL is allowed to
261 # execute multiple commands. We double check permissions of each command
261 # execute multiple commands. We double check permissions of each command
262 # as it is invoked to ensure there is no privilege escalation.
262 # as it is invoked to ensure there is no privilege escalation.
263 # TODO consider allowing multiple commands to regular command URLs
263 # TODO consider allowing multiple commands to regular command URLs
264 # iff each command is the same.
264 # iff each command is the same.
265
265
266 proto = httpv2protocolhandler(req, ui, args=command['args'])
266 proto = httpv2protocolhandler(req, ui, args=command['args'])
267
267
268 if reqcommand == b'multirequest':
268 if reqcommand == b'multirequest':
269 if not COMMANDS.commandavailable(command['command'], proto):
269 if not COMMANDS.commandavailable(command['command'], proto):
270 # TODO proper error mechanism
270 # TODO proper error mechanism
271 res.status = b'200 OK'
271 res.status = b'200 OK'
272 res.headers[b'Content-Type'] = b'text/plain'
272 res.headers[b'Content-Type'] = b'text/plain'
273 res.setbodybytes(_('wire protocol command not available: %s') %
273 res.setbodybytes(_('wire protocol command not available: %s') %
274 command['command'])
274 command['command'])
275 return True
275 return True
276
276
277 # TODO don't use assert here, since it may be elided by -O.
277 # TODO don't use assert here, since it may be elided by -O.
278 assert authedperm in (b'ro', b'rw')
278 assert authedperm in (b'ro', b'rw')
279 wirecommand = COMMANDS[command['command']]
279 wirecommand = COMMANDS[command['command']]
280 assert wirecommand.permission in ('push', 'pull')
280 assert wirecommand.permission in ('push', 'pull')
281
281
282 if authedperm == b'ro' and wirecommand.permission != 'pull':
282 if authedperm == b'ro' and wirecommand.permission != 'pull':
283 # TODO proper error mechanism
283 # TODO proper error mechanism
284 res.status = b'403 Forbidden'
284 res.status = b'403 Forbidden'
285 res.headers[b'Content-Type'] = b'text/plain'
285 res.headers[b'Content-Type'] = b'text/plain'
286 res.setbodybytes(_('insufficient permissions to execute '
286 res.setbodybytes(_('insufficient permissions to execute '
287 'command: %s') % command['command'])
287 'command: %s') % command['command'])
288 return True
288 return True
289
289
290 # TODO should we also call checkperm() here? Maybe not if we're going
290 # TODO should we also call checkperm() here? Maybe not if we're going
291 # to overhaul that API. The granted scope from the URL check should
291 # to overhaul that API. The granted scope from the URL check should
292 # be good enough.
292 # be good enough.
293
293
294 else:
294 else:
295 # Don't allow multiple commands outside of ``multirequest`` URL.
295 # Don't allow multiple commands outside of ``multirequest`` URL.
296 if issubsequent:
296 if issubsequent:
297 # TODO proper error mechanism
297 # TODO proper error mechanism
298 res.status = b'200 OK'
298 res.status = b'200 OK'
299 res.headers[b'Content-Type'] = b'text/plain'
299 res.headers[b'Content-Type'] = b'text/plain'
300 res.setbodybytes(_('multiple commands cannot be issued to this '
300 res.setbodybytes(_('multiple commands cannot be issued to this '
301 'URL'))
301 'URL'))
302 return True
302 return True
303
303
304 if reqcommand != command['command']:
304 if reqcommand != command['command']:
305 # TODO define proper error mechanism
305 # TODO define proper error mechanism
306 res.status = b'200 OK'
306 res.status = b'200 OK'
307 res.headers[b'Content-Type'] = b'text/plain'
307 res.headers[b'Content-Type'] = b'text/plain'
308 res.setbodybytes(_('command in frame must match command in URL'))
308 res.setbodybytes(_('command in frame must match command in URL'))
309 return True
309 return True
310
310
311 res.status = b'200 OK'
311 res.status = b'200 OK'
312 res.headers[b'Content-Type'] = FRAMINGTYPE
312 res.headers[b'Content-Type'] = FRAMINGTYPE
313
313
314 try:
314 try:
315 objs = dispatch(repo, proto, command['command'])
315 objs = dispatch(repo, proto, command['command'])
316
316
317 action, meta = reactor.oncommandresponsereadyobjects(
317 action, meta = reactor.oncommandresponsereadyobjects(
318 outstream, command['requestid'], objs)
318 outstream, command['requestid'], objs)
319
319
320 except error.WireprotoCommandError as e:
320 except error.WireprotoCommandError as e:
321 action, meta = reactor.oncommanderror(
321 action, meta = reactor.oncommanderror(
322 outstream, command['requestid'], e.message, e.messageargs)
322 outstream, command['requestid'], e.message, e.messageargs)
323
323
324 except Exception as e:
324 except Exception as e:
325 action, meta = reactor.onservererror(
325 action, meta = reactor.onservererror(
326 outstream, command['requestid'],
326 outstream, command['requestid'],
327 _('exception when invoking command: %s') %
327 _('exception when invoking command: %s') %
328 stringutil.forcebytestr(e))
328 stringutil.forcebytestr(e))
329
329
330 if action == 'sendframes':
330 if action == 'sendframes':
331 res.setbodygen(meta['framegen'])
331 res.setbodygen(meta['framegen'])
332 return True
332 return True
333 elif action == 'noop':
333 elif action == 'noop':
334 return False
334 return False
335 else:
335 else:
336 raise error.ProgrammingError('unhandled event from reactor: %s' %
336 raise error.ProgrammingError('unhandled event from reactor: %s' %
337 action)
337 action)
338
338
339 def getdispatchrepo(repo, proto, command):
339 def getdispatchrepo(repo, proto, command):
340 return repo.filtered('served')
340 return repo.filtered('served')
341
341
342 def dispatch(repo, proto, command):
342 def dispatch(repo, proto, command):
343 """Run a wire protocol command.
343 """Run a wire protocol command.
344
344
345 Returns an iterable of objects that will be sent to the client.
345 Returns an iterable of objects that will be sent to the client.
346 """
346 """
347 repo = getdispatchrepo(repo, proto, command)
347 repo = getdispatchrepo(repo, proto, command)
348
348
349 entry = COMMANDS[command]
349 entry = COMMANDS[command]
350 func = entry.func
350 func = entry.func
351 spec = entry.args
351 spec = entry.args
352
352
353 args = proto.getargs(spec)
353 args = proto.getargs(spec)
354
354
355 # There is some duplicate boilerplate code here for calling the command and
355 # There is some duplicate boilerplate code here for calling the command and
356 # emitting objects. It is either that or a lot of indented code that looks
356 # emitting objects. It is either that or a lot of indented code that looks
357 # like a pyramid (since there are a lot of code paths that result in not
357 # like a pyramid (since there are a lot of code paths that result in not
358 # using the cacher).
358 # using the cacher).
359 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
359 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
360
360
361 # Request is not cacheable. Don't bother instantiating a cacher.
361 # Request is not cacheable. Don't bother instantiating a cacher.
362 if not entry.cachekeyfn:
362 if not entry.cachekeyfn:
363 for o in callcommand():
363 for o in callcommand():
364 yield o
364 yield o
365 return
365 return
366
366
367 cacher = makeresponsecacher(repo, proto, command, args,
367 cacher = makeresponsecacher(repo, proto, command, args,
368 cborutil.streamencode)
368 cborutil.streamencode)
369
369
370 # But we have no cacher. Do default handling.
370 # But we have no cacher. Do default handling.
371 if not cacher:
371 if not cacher:
372 for o in callcommand():
372 for o in callcommand():
373 yield o
373 yield o
374 return
374 return
375
375
376 with cacher:
376 with cacher:
377 cachekey = entry.cachekeyfn(repo, proto, cacher, **args)
377 cachekey = entry.cachekeyfn(repo, proto, cacher, **args)
378
378
379 # No cache key or the cacher doesn't like it. Do default handling.
379 # No cache key or the cacher doesn't like it. Do default handling.
380 if cachekey is None or not cacher.setcachekey(cachekey):
380 if cachekey is None or not cacher.setcachekey(cachekey):
381 for o in callcommand():
381 for o in callcommand():
382 yield o
382 yield o
383 return
383 return
384
384
385 # Serve it from the cache, if possible.
385 # Serve it from the cache, if possible.
386 cached = cacher.lookup()
386 cached = cacher.lookup()
387
387
388 if cached:
388 if cached:
389 for o in cached['objs']:
389 for o in cached['objs']:
390 yield o
390 yield o
391 return
391 return
392
392
393 # Else call the command and feed its output into the cacher, allowing
393 # Else call the command and feed its output into the cacher, allowing
394 # the cacher to buffer/mutate objects as it desires.
394 # the cacher to buffer/mutate objects as it desires.
395 for o in callcommand():
395 for o in callcommand():
396 for o in cacher.onobject(o):
396 for o in cacher.onobject(o):
397 yield o
397 yield o
398
398
399 for o in cacher.onfinished():
399 for o in cacher.onfinished():
400 yield o
400 yield o
401
401
402 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
402 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
403 class httpv2protocolhandler(object):
403 class httpv2protocolhandler(object):
404 def __init__(self, req, ui, args=None):
404 def __init__(self, req, ui, args=None):
405 self._req = req
405 self._req = req
406 self._ui = ui
406 self._ui = ui
407 self._args = args
407 self._args = args
408
408
409 @property
409 @property
410 def name(self):
410 def name(self):
411 return HTTP_WIREPROTO_V2
411 return HTTP_WIREPROTO_V2
412
412
413 def getargs(self, args):
413 def getargs(self, args):
414 # First look for args that were passed but aren't registered on this
414 # First look for args that were passed but aren't registered on this
415 # command.
415 # command.
416 extra = set(self._args) - set(args)
416 extra = set(self._args) - set(args)
417 if extra:
417 if extra:
418 raise error.WireprotoCommandError(
418 raise error.WireprotoCommandError(
419 'unsupported argument to command: %s' %
419 'unsupported argument to command: %s' %
420 ', '.join(sorted(extra)))
420 ', '.join(sorted(extra)))
421
421
422 # And look for required arguments that are missing.
422 # And look for required arguments that are missing.
423 missing = {a for a in args if args[a]['required']} - set(self._args)
423 missing = {a for a in args if args[a]['required']} - set(self._args)
424
424
425 if missing:
425 if missing:
426 raise error.WireprotoCommandError(
426 raise error.WireprotoCommandError(
427 'missing required arguments: %s' % ', '.join(sorted(missing)))
427 'missing required arguments: %s' % ', '.join(sorted(missing)))
428
428
429 # Now derive the arguments to pass to the command, taking into
429 # Now derive the arguments to pass to the command, taking into
430 # account the arguments specified by the client.
430 # account the arguments specified by the client.
431 data = {}
431 data = {}
432 for k, meta in sorted(args.items()):
432 for k, meta in sorted(args.items()):
433 # This argument wasn't passed by the client.
433 # This argument wasn't passed by the client.
434 if k not in self._args:
434 if k not in self._args:
435 data[k] = meta['default']()
435 data[k] = meta['default']()
436 continue
436 continue
437
437
438 v = self._args[k]
438 v = self._args[k]
439
439
440 # Sets may be expressed as lists. Silently normalize.
440 # Sets may be expressed as lists. Silently normalize.
441 if meta['type'] == 'set' and isinstance(v, list):
441 if meta['type'] == 'set' and isinstance(v, list):
442 v = set(v)
442 v = set(v)
443
443
444 # TODO consider more/stronger type validation.
444 # TODO consider more/stronger type validation.
445
445
446 data[k] = v
446 data[k] = v
447
447
448 return data
448 return data
449
449
450 def getprotocaps(self):
450 def getprotocaps(self):
451 # Protocol capabilities are currently not implemented for HTTP V2.
451 # Protocol capabilities are currently not implemented for HTTP V2.
452 return set()
452 return set()
453
453
454 def getpayload(self):
454 def getpayload(self):
455 raise NotImplementedError
455 raise NotImplementedError
456
456
457 @contextlib.contextmanager
457 @contextlib.contextmanager
458 def mayberedirectstdio(self):
458 def mayberedirectstdio(self):
459 raise NotImplementedError
459 raise NotImplementedError
460
460
461 def client(self):
461 def client(self):
462 raise NotImplementedError
462 raise NotImplementedError
463
463
464 def addcapabilities(self, repo, caps):
464 def addcapabilities(self, repo, caps):
465 return caps
465 return caps
466
466
467 def checkperm(self, perm):
467 def checkperm(self, perm):
468 raise NotImplementedError
468 raise NotImplementedError
469
469
470 def httpv2apidescriptor(req, repo):
470 def httpv2apidescriptor(req, repo):
471 proto = httpv2protocolhandler(req, repo.ui)
471 proto = httpv2protocolhandler(req, repo.ui)
472
472
473 return _capabilitiesv2(repo, proto)
473 return _capabilitiesv2(repo, proto)
474
474
475 def _capabilitiesv2(repo, proto):
475 def _capabilitiesv2(repo, proto):
476 """Obtain the set of capabilities for version 2 transports.
476 """Obtain the set of capabilities for version 2 transports.
477
477
478 These capabilities are distinct from the capabilities for version 1
478 These capabilities are distinct from the capabilities for version 1
479 transports.
479 transports.
480 """
480 """
481 compression = []
481 compression = []
482 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
482 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
483 compression.append({
483 compression.append({
484 b'name': engine.wireprotosupport().name,
484 b'name': engine.wireprotosupport().name,
485 })
485 })
486
486
487 caps = {
487 caps = {
488 'commands': {},
488 'commands': {},
489 'compression': compression,
489 'compression': compression,
490 'framingmediatypes': [FRAMINGTYPE],
490 'framingmediatypes': [FRAMINGTYPE],
491 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
491 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
492 }
492 }
493
493
494 for command, entry in COMMANDS.items():
494 for command, entry in COMMANDS.items():
495 args = {}
495 args = {}
496
496
497 for arg, meta in entry.args.items():
497 for arg, meta in entry.args.items():
498 args[arg] = {
498 args[arg] = {
499 # TODO should this be a normalized type using CBOR's
499 # TODO should this be a normalized type using CBOR's
500 # terminology?
500 # terminology?
501 b'type': meta['type'],
501 b'type': meta['type'],
502 b'required': meta['required'],
502 b'required': meta['required'],
503 }
503 }
504
504
505 if not meta['required']:
505 if not meta['required']:
506 args[arg][b'default'] = meta['default']()
506 args[arg][b'default'] = meta['default']()
507
507
508 if meta['validvalues']:
508 if meta['validvalues']:
509 args[arg][b'validvalues'] = meta['validvalues']
509 args[arg][b'validvalues'] = meta['validvalues']
510
510
511 caps['commands'][command] = {
511 caps['commands'][command] = {
512 'args': args,
512 'args': args,
513 'permissions': [entry.permission],
513 'permissions': [entry.permission],
514 }
514 }
515
515
516 if streamclone.allowservergeneration(repo):
516 if streamclone.allowservergeneration(repo):
517 caps['rawrepoformats'] = sorted(repo.requirements &
517 caps['rawrepoformats'] = sorted(repo.requirements &
518 repo.supportedformats)
518 repo.supportedformats)
519
519
520 targets = getadvertisedredirecttargets(repo, proto)
521 if targets:
522 caps[b'redirect'] = {
523 b'targets': [],
524 b'hashes': [b'sha256', b'sha1'],
525 }
526
527 for target in targets:
528 entry = {
529 b'name': target['name'],
530 b'protocol': target['protocol'],
531 b'uris': target['uris'],
532 }
533
534 for key in ('snirequired', 'tlsversions'):
535 if key in target:
536 entry[key] = target[key]
537
538 caps[b'redirect'][b'targets'].append(entry)
539
520 return proto.addcapabilities(repo, caps)
540 return proto.addcapabilities(repo, caps)
521
541
542 def getadvertisedredirecttargets(repo, proto):
543 """Obtain a list of content redirect targets.
544
545 Returns a list containing potential redirect targets that will be
546 advertised in capabilities data. Each dict MUST have the following
547 keys:
548
549 name
550 The name of this redirect target. This is the identifier clients use
551 to refer to a target. It is transferred as part of every command
552 request.
553
554 protocol
555 Network protocol used by this target. Typically this is the string
556 in front of the ``://`` in a URL. e.g. ``https``.
557
558 uris
559 List of representative URIs for this target. Clients can use the
560 URIs to test parsing for compatibility or for ordering preference
561 for which target to use.
562
563 The following optional keys are recognized:
564
565 snirequired
566 Bool indicating if Server Name Indication (SNI) is required to
567 connect to this target.
568
569 tlsversions
570 List of bytes indicating which TLS versions are supported by this
571 target.
572
573 By default, clients reflect the target order advertised by servers
574 and servers will use the first client-advertised target when picking
575 a redirect target. So targets should be advertised in the order the
576 server prefers they be used.
577 """
578 return []
579
522 def wireprotocommand(name, args=None, permission='push', cachekeyfn=None):
580 def wireprotocommand(name, args=None, permission='push', cachekeyfn=None):
523 """Decorator to declare a wire protocol command.
581 """Decorator to declare a wire protocol command.
524
582
525 ``name`` is the name of the wire protocol command being provided.
583 ``name`` is the name of the wire protocol command being provided.
526
584
527 ``args`` is a dict defining arguments accepted by the command. Keys are
585 ``args`` is a dict defining arguments accepted by the command. Keys are
528 the argument name. Values are dicts with the following keys:
586 the argument name. Values are dicts with the following keys:
529
587
530 ``type``
588 ``type``
531 The argument data type. Must be one of the following string
589 The argument data type. Must be one of the following string
532 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
590 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
533 or ``bool``.
591 or ``bool``.
534
592
535 ``default``
593 ``default``
536 A callable returning the default value for this argument. If not
594 A callable returning the default value for this argument. If not
537 specified, ``None`` will be the default value.
595 specified, ``None`` will be the default value.
538
596
539 ``example``
597 ``example``
540 An example value for this argument.
598 An example value for this argument.
541
599
542 ``validvalues``
600 ``validvalues``
543 Set of recognized values for this argument.
601 Set of recognized values for this argument.
544
602
545 ``permission`` defines the permission type needed to run this command.
603 ``permission`` defines the permission type needed to run this command.
546 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
604 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
547 respectively. Default is to assume command requires ``push`` permissions
605 respectively. Default is to assume command requires ``push`` permissions
548 because otherwise commands not declaring their permissions could modify
606 because otherwise commands not declaring their permissions could modify
549 a repository that is supposed to be read-only.
607 a repository that is supposed to be read-only.
550
608
551 ``cachekeyfn`` defines an optional callable that can derive the
609 ``cachekeyfn`` defines an optional callable that can derive the
552 cache key for this request.
610 cache key for this request.
553
611
554 Wire protocol commands are generators of objects to be serialized and
612 Wire protocol commands are generators of objects to be serialized and
555 sent to the client.
613 sent to the client.
556
614
557 If a command raises an uncaught exception, this will be translated into
615 If a command raises an uncaught exception, this will be translated into
558 a command error.
616 a command error.
559
617
560 All commands can opt in to being cacheable by defining a function
618 All commands can opt in to being cacheable by defining a function
561 (``cachekeyfn``) that is called to derive a cache key. This function
619 (``cachekeyfn``) that is called to derive a cache key. This function
562 receives the same arguments as the command itself plus a ``cacher``
620 receives the same arguments as the command itself plus a ``cacher``
563 argument containing the active cacher for the request and returns a bytes
621 argument containing the active cacher for the request and returns a bytes
564 containing the key in a cache the response to this command may be cached
622 containing the key in a cache the response to this command may be cached
565 under.
623 under.
566 """
624 """
567 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
625 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
568 if v['version'] == 2}
626 if v['version'] == 2}
569
627
570 if permission not in ('push', 'pull'):
628 if permission not in ('push', 'pull'):
571 raise error.ProgrammingError('invalid wire protocol permission; '
629 raise error.ProgrammingError('invalid wire protocol permission; '
572 'got %s; expected "push" or "pull"' %
630 'got %s; expected "push" or "pull"' %
573 permission)
631 permission)
574
632
575 if args is None:
633 if args is None:
576 args = {}
634 args = {}
577
635
578 if not isinstance(args, dict):
636 if not isinstance(args, dict):
579 raise error.ProgrammingError('arguments for version 2 commands '
637 raise error.ProgrammingError('arguments for version 2 commands '
580 'must be declared as dicts')
638 'must be declared as dicts')
581
639
582 for arg, meta in args.items():
640 for arg, meta in args.items():
583 if arg == '*':
641 if arg == '*':
584 raise error.ProgrammingError('* argument name not allowed on '
642 raise error.ProgrammingError('* argument name not allowed on '
585 'version 2 commands')
643 'version 2 commands')
586
644
587 if not isinstance(meta, dict):
645 if not isinstance(meta, dict):
588 raise error.ProgrammingError('arguments for version 2 commands '
646 raise error.ProgrammingError('arguments for version 2 commands '
589 'must declare metadata as a dict')
647 'must declare metadata as a dict')
590
648
591 if 'type' not in meta:
649 if 'type' not in meta:
592 raise error.ProgrammingError('%s argument for command %s does not '
650 raise error.ProgrammingError('%s argument for command %s does not '
593 'declare type field' % (arg, name))
651 'declare type field' % (arg, name))
594
652
595 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
653 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
596 raise error.ProgrammingError('%s argument for command %s has '
654 raise error.ProgrammingError('%s argument for command %s has '
597 'illegal type: %s' % (arg, name,
655 'illegal type: %s' % (arg, name,
598 meta['type']))
656 meta['type']))
599
657
600 if 'example' not in meta:
658 if 'example' not in meta:
601 raise error.ProgrammingError('%s argument for command %s does not '
659 raise error.ProgrammingError('%s argument for command %s does not '
602 'declare example field' % (arg, name))
660 'declare example field' % (arg, name))
603
661
604 meta['required'] = 'default' not in meta
662 meta['required'] = 'default' not in meta
605
663
606 meta.setdefault('default', lambda: None)
664 meta.setdefault('default', lambda: None)
607 meta.setdefault('validvalues', None)
665 meta.setdefault('validvalues', None)
608
666
609 def register(func):
667 def register(func):
610 if name in COMMANDS:
668 if name in COMMANDS:
611 raise error.ProgrammingError('%s command already registered '
669 raise error.ProgrammingError('%s command already registered '
612 'for version 2' % name)
670 'for version 2' % name)
613
671
614 COMMANDS[name] = wireprototypes.commandentry(
672 COMMANDS[name] = wireprototypes.commandentry(
615 func, args=args, transports=transports, permission=permission,
673 func, args=args, transports=transports, permission=permission,
616 cachekeyfn=cachekeyfn)
674 cachekeyfn=cachekeyfn)
617
675
618 return func
676 return func
619
677
620 return register
678 return register
621
679
622 def makecommandcachekeyfn(command, localversion=None, allargs=False):
680 def makecommandcachekeyfn(command, localversion=None, allargs=False):
623 """Construct a cache key derivation function with common features.
681 """Construct a cache key derivation function with common features.
624
682
625 By default, the cache key is a hash of:
683 By default, the cache key is a hash of:
626
684
627 * The command name.
685 * The command name.
628 * A global cache version number.
686 * A global cache version number.
629 * A local cache version number (passed via ``localversion``).
687 * A local cache version number (passed via ``localversion``).
630 * All the arguments passed to the command.
688 * All the arguments passed to the command.
631 * The media type used.
689 * The media type used.
632 * Wire protocol version string.
690 * Wire protocol version string.
633 * The repository path.
691 * The repository path.
634 """
692 """
635 if not allargs:
693 if not allargs:
636 raise error.ProgrammingError('only allargs=True is currently supported')
694 raise error.ProgrammingError('only allargs=True is currently supported')
637
695
638 if localversion is None:
696 if localversion is None:
639 raise error.ProgrammingError('must set localversion argument value')
697 raise error.ProgrammingError('must set localversion argument value')
640
698
641 def cachekeyfn(repo, proto, cacher, **args):
699 def cachekeyfn(repo, proto, cacher, **args):
642 spec = COMMANDS[command]
700 spec = COMMANDS[command]
643
701
644 # Commands that mutate the repo can not be cached.
702 # Commands that mutate the repo can not be cached.
645 if spec.permission == 'push':
703 if spec.permission == 'push':
646 return None
704 return None
647
705
648 # TODO config option to disable caching.
706 # TODO config option to disable caching.
649
707
650 # Our key derivation strategy is to construct a data structure
708 # Our key derivation strategy is to construct a data structure
651 # holding everything that could influence cacheability and to hash
709 # holding everything that could influence cacheability and to hash
652 # the CBOR representation of that. Using CBOR seems like it might
710 # the CBOR representation of that. Using CBOR seems like it might
653 # be overkill. However, simpler hashing mechanisms are prone to
711 # be overkill. However, simpler hashing mechanisms are prone to
654 # duplicate input issues. e.g. if you just concatenate two values,
712 # duplicate input issues. e.g. if you just concatenate two values,
655 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
713 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
656 # "padding" between values and prevents these problems.
714 # "padding" between values and prevents these problems.
657
715
658 # Seed the hash with various data.
716 # Seed the hash with various data.
659 state = {
717 state = {
660 # To invalidate all cache keys.
718 # To invalidate all cache keys.
661 b'globalversion': GLOBAL_CACHE_VERSION,
719 b'globalversion': GLOBAL_CACHE_VERSION,
662 # More granular cache key invalidation.
720 # More granular cache key invalidation.
663 b'localversion': localversion,
721 b'localversion': localversion,
664 # Cache keys are segmented by command.
722 # Cache keys are segmented by command.
665 b'command': pycompat.sysbytes(command),
723 b'command': pycompat.sysbytes(command),
666 # Throw in the media type and API version strings so changes
724 # Throw in the media type and API version strings so changes
667 # to exchange semantics invalid cache.
725 # to exchange semantics invalid cache.
668 b'mediatype': FRAMINGTYPE,
726 b'mediatype': FRAMINGTYPE,
669 b'version': HTTP_WIREPROTO_V2,
727 b'version': HTTP_WIREPROTO_V2,
670 # So same requests for different repos don't share cache keys.
728 # So same requests for different repos don't share cache keys.
671 b'repo': repo.root,
729 b'repo': repo.root,
672 }
730 }
673
731
674 # The arguments passed to us will have already been normalized.
732 # The arguments passed to us will have already been normalized.
675 # Default values will be set, etc. This is important because it
733 # Default values will be set, etc. This is important because it
676 # means that it doesn't matter if clients send an explicit argument
734 # means that it doesn't matter if clients send an explicit argument
677 # or rely on the default value: it will all normalize to the same
735 # or rely on the default value: it will all normalize to the same
678 # set of arguments on the server and therefore the same cache key.
736 # set of arguments on the server and therefore the same cache key.
679 #
737 #
680 # Arguments by their very nature must support being encoded to CBOR.
738 # Arguments by their very nature must support being encoded to CBOR.
681 # And the CBOR encoder is deterministic. So we hash the arguments
739 # And the CBOR encoder is deterministic. So we hash the arguments
682 # by feeding the CBOR of their representation into the hasher.
740 # by feeding the CBOR of their representation into the hasher.
683 if allargs:
741 if allargs:
684 state[b'args'] = pycompat.byteskwargs(args)
742 state[b'args'] = pycompat.byteskwargs(args)
685
743
686 cacher.adjustcachekeystate(state)
744 cacher.adjustcachekeystate(state)
687
745
688 hasher = hashlib.sha1()
746 hasher = hashlib.sha1()
689 for chunk in cborutil.streamencode(state):
747 for chunk in cborutil.streamencode(state):
690 hasher.update(chunk)
748 hasher.update(chunk)
691
749
692 return pycompat.sysbytes(hasher.hexdigest())
750 return pycompat.sysbytes(hasher.hexdigest())
693
751
694 return cachekeyfn
752 return cachekeyfn
695
753
696 def makeresponsecacher(repo, proto, command, args, objencoderfn):
754 def makeresponsecacher(repo, proto, command, args, objencoderfn):
697 """Construct a cacher for a cacheable command.
755 """Construct a cacher for a cacheable command.
698
756
699 Returns an ``iwireprotocolcommandcacher`` instance.
757 Returns an ``iwireprotocolcommandcacher`` instance.
700
758
701 Extensions can monkeypatch this function to provide custom caching
759 Extensions can monkeypatch this function to provide custom caching
702 backends.
760 backends.
703 """
761 """
704 return None
762 return None
705
763
706 @wireprotocommand('branchmap', permission='pull')
764 @wireprotocommand('branchmap', permission='pull')
707 def branchmapv2(repo, proto):
765 def branchmapv2(repo, proto):
708 yield {encoding.fromlocal(k): v
766 yield {encoding.fromlocal(k): v
709 for k, v in repo.branchmap().iteritems()}
767 for k, v in repo.branchmap().iteritems()}
710
768
711 @wireprotocommand('capabilities', permission='pull')
769 @wireprotocommand('capabilities', permission='pull')
712 def capabilitiesv2(repo, proto):
770 def capabilitiesv2(repo, proto):
713 yield _capabilitiesv2(repo, proto)
771 yield _capabilitiesv2(repo, proto)
714
772
715 @wireprotocommand(
773 @wireprotocommand(
716 'changesetdata',
774 'changesetdata',
717 args={
775 args={
718 'noderange': {
776 'noderange': {
719 'type': 'list',
777 'type': 'list',
720 'default': lambda: None,
778 'default': lambda: None,
721 'example': [[b'0123456...'], [b'abcdef...']],
779 'example': [[b'0123456...'], [b'abcdef...']],
722 },
780 },
723 'nodes': {
781 'nodes': {
724 'type': 'list',
782 'type': 'list',
725 'default': lambda: None,
783 'default': lambda: None,
726 'example': [b'0123456...'],
784 'example': [b'0123456...'],
727 },
785 },
728 'nodesdepth': {
786 'nodesdepth': {
729 'type': 'int',
787 'type': 'int',
730 'default': lambda: None,
788 'default': lambda: None,
731 'example': 10,
789 'example': 10,
732 },
790 },
733 'fields': {
791 'fields': {
734 'type': 'set',
792 'type': 'set',
735 'default': set,
793 'default': set,
736 'example': {b'parents', b'revision'},
794 'example': {b'parents', b'revision'},
737 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
795 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
738 },
796 },
739 },
797 },
740 permission='pull')
798 permission='pull')
741 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
799 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
742 # TODO look for unknown fields and abort when they can't be serviced.
800 # TODO look for unknown fields and abort when they can't be serviced.
743 # This could probably be validated by dispatcher using validvalues.
801 # This could probably be validated by dispatcher using validvalues.
744
802
745 if noderange is None and nodes is None:
803 if noderange is None and nodes is None:
746 raise error.WireprotoCommandError(
804 raise error.WireprotoCommandError(
747 'noderange or nodes must be defined')
805 'noderange or nodes must be defined')
748
806
749 if nodesdepth is not None and nodes is None:
807 if nodesdepth is not None and nodes is None:
750 raise error.WireprotoCommandError(
808 raise error.WireprotoCommandError(
751 'nodesdepth requires the nodes argument')
809 'nodesdepth requires the nodes argument')
752
810
753 if noderange is not None:
811 if noderange is not None:
754 if len(noderange) != 2:
812 if len(noderange) != 2:
755 raise error.WireprotoCommandError(
813 raise error.WireprotoCommandError(
756 'noderange must consist of 2 elements')
814 'noderange must consist of 2 elements')
757
815
758 if not noderange[1]:
816 if not noderange[1]:
759 raise error.WireprotoCommandError(
817 raise error.WireprotoCommandError(
760 'heads in noderange request cannot be empty')
818 'heads in noderange request cannot be empty')
761
819
762 cl = repo.changelog
820 cl = repo.changelog
763 hasnode = cl.hasnode
821 hasnode = cl.hasnode
764
822
765 seen = set()
823 seen = set()
766 outgoing = []
824 outgoing = []
767
825
768 if nodes is not None:
826 if nodes is not None:
769 outgoing = [n for n in nodes if hasnode(n)]
827 outgoing = [n for n in nodes if hasnode(n)]
770
828
771 if nodesdepth:
829 if nodesdepth:
772 outgoing = [cl.node(r) for r in
830 outgoing = [cl.node(r) for r in
773 repo.revs(b'ancestors(%ln, %d)', outgoing,
831 repo.revs(b'ancestors(%ln, %d)', outgoing,
774 nodesdepth - 1)]
832 nodesdepth - 1)]
775
833
776 seen |= set(outgoing)
834 seen |= set(outgoing)
777
835
778 if noderange is not None:
836 if noderange is not None:
779 if noderange[0]:
837 if noderange[0]:
780 common = [n for n in noderange[0] if hasnode(n)]
838 common = [n for n in noderange[0] if hasnode(n)]
781 else:
839 else:
782 common = [nullid]
840 common = [nullid]
783
841
784 for n in discovery.outgoing(repo, common, noderange[1]).missing:
842 for n in discovery.outgoing(repo, common, noderange[1]).missing:
785 if n not in seen:
843 if n not in seen:
786 outgoing.append(n)
844 outgoing.append(n)
787 # Don't need to add to seen here because this is the final
845 # Don't need to add to seen here because this is the final
788 # source of nodes and there should be no duplicates in this
846 # source of nodes and there should be no duplicates in this
789 # list.
847 # list.
790
848
791 seen.clear()
849 seen.clear()
792 publishing = repo.publishing()
850 publishing = repo.publishing()
793
851
794 if outgoing:
852 if outgoing:
795 repo.hook('preoutgoing', throw=True, source='serve')
853 repo.hook('preoutgoing', throw=True, source='serve')
796
854
797 yield {
855 yield {
798 b'totalitems': len(outgoing),
856 b'totalitems': len(outgoing),
799 }
857 }
800
858
801 # The phases of nodes already transferred to the client may have changed
859 # The phases of nodes already transferred to the client may have changed
802 # since the client last requested data. We send phase-only records
860 # since the client last requested data. We send phase-only records
803 # for these revisions, if requested.
861 # for these revisions, if requested.
804 if b'phase' in fields and noderange is not None:
862 if b'phase' in fields and noderange is not None:
805 # TODO skip nodes whose phase will be reflected by a node in the
863 # TODO skip nodes whose phase will be reflected by a node in the
806 # outgoing set. This is purely an optimization to reduce data
864 # outgoing set. This is purely an optimization to reduce data
807 # size.
865 # size.
808 for node in noderange[0]:
866 for node in noderange[0]:
809 yield {
867 yield {
810 b'node': node,
868 b'node': node,
811 b'phase': b'public' if publishing else repo[node].phasestr()
869 b'phase': b'public' if publishing else repo[node].phasestr()
812 }
870 }
813
871
814 nodebookmarks = {}
872 nodebookmarks = {}
815 for mark, node in repo._bookmarks.items():
873 for mark, node in repo._bookmarks.items():
816 nodebookmarks.setdefault(node, set()).add(mark)
874 nodebookmarks.setdefault(node, set()).add(mark)
817
875
818 # It is already topologically sorted by revision number.
876 # It is already topologically sorted by revision number.
819 for node in outgoing:
877 for node in outgoing:
820 d = {
878 d = {
821 b'node': node,
879 b'node': node,
822 }
880 }
823
881
824 if b'parents' in fields:
882 if b'parents' in fields:
825 d[b'parents'] = cl.parents(node)
883 d[b'parents'] = cl.parents(node)
826
884
827 if b'phase' in fields:
885 if b'phase' in fields:
828 if publishing:
886 if publishing:
829 d[b'phase'] = b'public'
887 d[b'phase'] = b'public'
830 else:
888 else:
831 ctx = repo[node]
889 ctx = repo[node]
832 d[b'phase'] = ctx.phasestr()
890 d[b'phase'] = ctx.phasestr()
833
891
834 if b'bookmarks' in fields and node in nodebookmarks:
892 if b'bookmarks' in fields and node in nodebookmarks:
835 d[b'bookmarks'] = sorted(nodebookmarks[node])
893 d[b'bookmarks'] = sorted(nodebookmarks[node])
836 del nodebookmarks[node]
894 del nodebookmarks[node]
837
895
838 followingmeta = []
896 followingmeta = []
839 followingdata = []
897 followingdata = []
840
898
841 if b'revision' in fields:
899 if b'revision' in fields:
842 revisiondata = cl.revision(node, raw=True)
900 revisiondata = cl.revision(node, raw=True)
843 followingmeta.append((b'revision', len(revisiondata)))
901 followingmeta.append((b'revision', len(revisiondata)))
844 followingdata.append(revisiondata)
902 followingdata.append(revisiondata)
845
903
846 # TODO make it possible for extensions to wrap a function or register
904 # TODO make it possible for extensions to wrap a function or register
847 # a handler to service custom fields.
905 # a handler to service custom fields.
848
906
849 if followingmeta:
907 if followingmeta:
850 d[b'fieldsfollowing'] = followingmeta
908 d[b'fieldsfollowing'] = followingmeta
851
909
852 yield d
910 yield d
853
911
854 for extra in followingdata:
912 for extra in followingdata:
855 yield extra
913 yield extra
856
914
857 # If requested, send bookmarks from nodes that didn't have revision
915 # If requested, send bookmarks from nodes that didn't have revision
858 # data sent so receiver is aware of any bookmark updates.
916 # data sent so receiver is aware of any bookmark updates.
859 if b'bookmarks' in fields:
917 if b'bookmarks' in fields:
860 for node, marks in sorted(nodebookmarks.iteritems()):
918 for node, marks in sorted(nodebookmarks.iteritems()):
861 yield {
919 yield {
862 b'node': node,
920 b'node': node,
863 b'bookmarks': sorted(marks),
921 b'bookmarks': sorted(marks),
864 }
922 }
865
923
866 class FileAccessError(Exception):
924 class FileAccessError(Exception):
867 """Represents an error accessing a specific file."""
925 """Represents an error accessing a specific file."""
868
926
869 def __init__(self, path, msg, args):
927 def __init__(self, path, msg, args):
870 self.path = path
928 self.path = path
871 self.msg = msg
929 self.msg = msg
872 self.args = args
930 self.args = args
873
931
874 def getfilestore(repo, proto, path):
932 def getfilestore(repo, proto, path):
875 """Obtain a file storage object for use with wire protocol.
933 """Obtain a file storage object for use with wire protocol.
876
934
877 Exists as a standalone function so extensions can monkeypatch to add
935 Exists as a standalone function so extensions can monkeypatch to add
878 access control.
936 access control.
879 """
937 """
880 # This seems to work even if the file doesn't exist. So catch
938 # This seems to work even if the file doesn't exist. So catch
881 # "empty" files and return an error.
939 # "empty" files and return an error.
882 fl = repo.file(path)
940 fl = repo.file(path)
883
941
884 if not len(fl):
942 if not len(fl):
885 raise FileAccessError(path, 'unknown file: %s', (path,))
943 raise FileAccessError(path, 'unknown file: %s', (path,))
886
944
887 return fl
945 return fl
888
946
889 @wireprotocommand(
947 @wireprotocommand(
890 'filedata',
948 'filedata',
891 args={
949 args={
892 'haveparents': {
950 'haveparents': {
893 'type': 'bool',
951 'type': 'bool',
894 'default': lambda: False,
952 'default': lambda: False,
895 'example': True,
953 'example': True,
896 },
954 },
897 'nodes': {
955 'nodes': {
898 'type': 'list',
956 'type': 'list',
899 'example': [b'0123456...'],
957 'example': [b'0123456...'],
900 },
958 },
901 'fields': {
959 'fields': {
902 'type': 'set',
960 'type': 'set',
903 'default': set,
961 'default': set,
904 'example': {b'parents', b'revision'},
962 'example': {b'parents', b'revision'},
905 'validvalues': {b'parents', b'revision'},
963 'validvalues': {b'parents', b'revision'},
906 },
964 },
907 'path': {
965 'path': {
908 'type': 'bytes',
966 'type': 'bytes',
909 'example': b'foo.txt',
967 'example': b'foo.txt',
910 }
968 }
911 },
969 },
912 permission='pull',
970 permission='pull',
913 # TODO censoring a file revision won't invalidate the cache.
971 # TODO censoring a file revision won't invalidate the cache.
914 # Figure out a way to take censoring into account when deriving
972 # Figure out a way to take censoring into account when deriving
915 # the cache key.
973 # the cache key.
916 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True))
974 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True))
917 def filedata(repo, proto, haveparents, nodes, fields, path):
975 def filedata(repo, proto, haveparents, nodes, fields, path):
918 try:
976 try:
919 # Extensions may wish to access the protocol handler.
977 # Extensions may wish to access the protocol handler.
920 store = getfilestore(repo, proto, path)
978 store = getfilestore(repo, proto, path)
921 except FileAccessError as e:
979 except FileAccessError as e:
922 raise error.WireprotoCommandError(e.msg, e.args)
980 raise error.WireprotoCommandError(e.msg, e.args)
923
981
924 # Validate requested nodes.
982 # Validate requested nodes.
925 for node in nodes:
983 for node in nodes:
926 try:
984 try:
927 store.rev(node)
985 store.rev(node)
928 except error.LookupError:
986 except error.LookupError:
929 raise error.WireprotoCommandError('unknown file node: %s',
987 raise error.WireprotoCommandError('unknown file node: %s',
930 (hex(node),))
988 (hex(node),))
931
989
932 revisions = store.emitrevisions(nodes,
990 revisions = store.emitrevisions(nodes,
933 revisiondata=b'revision' in fields,
991 revisiondata=b'revision' in fields,
934 assumehaveparentrevisions=haveparents)
992 assumehaveparentrevisions=haveparents)
935
993
936 yield {
994 yield {
937 b'totalitems': len(nodes),
995 b'totalitems': len(nodes),
938 }
996 }
939
997
940 for revision in revisions:
998 for revision in revisions:
941 d = {
999 d = {
942 b'node': revision.node,
1000 b'node': revision.node,
943 }
1001 }
944
1002
945 if b'parents' in fields:
1003 if b'parents' in fields:
946 d[b'parents'] = [revision.p1node, revision.p2node]
1004 d[b'parents'] = [revision.p1node, revision.p2node]
947
1005
948 followingmeta = []
1006 followingmeta = []
949 followingdata = []
1007 followingdata = []
950
1008
951 if b'revision' in fields:
1009 if b'revision' in fields:
952 if revision.revision is not None:
1010 if revision.revision is not None:
953 followingmeta.append((b'revision', len(revision.revision)))
1011 followingmeta.append((b'revision', len(revision.revision)))
954 followingdata.append(revision.revision)
1012 followingdata.append(revision.revision)
955 else:
1013 else:
956 d[b'deltabasenode'] = revision.basenode
1014 d[b'deltabasenode'] = revision.basenode
957 followingmeta.append((b'delta', len(revision.delta)))
1015 followingmeta.append((b'delta', len(revision.delta)))
958 followingdata.append(revision.delta)
1016 followingdata.append(revision.delta)
959
1017
960 if followingmeta:
1018 if followingmeta:
961 d[b'fieldsfollowing'] = followingmeta
1019 d[b'fieldsfollowing'] = followingmeta
962
1020
963 yield d
1021 yield d
964
1022
965 for extra in followingdata:
1023 for extra in followingdata:
966 yield extra
1024 yield extra
967
1025
968 @wireprotocommand(
1026 @wireprotocommand(
969 'heads',
1027 'heads',
970 args={
1028 args={
971 'publiconly': {
1029 'publiconly': {
972 'type': 'bool',
1030 'type': 'bool',
973 'default': lambda: False,
1031 'default': lambda: False,
974 'example': False,
1032 'example': False,
975 },
1033 },
976 },
1034 },
977 permission='pull')
1035 permission='pull')
978 def headsv2(repo, proto, publiconly):
1036 def headsv2(repo, proto, publiconly):
979 if publiconly:
1037 if publiconly:
980 repo = repo.filtered('immutable')
1038 repo = repo.filtered('immutable')
981
1039
982 yield repo.heads()
1040 yield repo.heads()
983
1041
984 @wireprotocommand(
1042 @wireprotocommand(
985 'known',
1043 'known',
986 args={
1044 args={
987 'nodes': {
1045 'nodes': {
988 'type': 'list',
1046 'type': 'list',
989 'default': list,
1047 'default': list,
990 'example': [b'deadbeef'],
1048 'example': [b'deadbeef'],
991 },
1049 },
992 },
1050 },
993 permission='pull')
1051 permission='pull')
994 def knownv2(repo, proto, nodes):
1052 def knownv2(repo, proto, nodes):
995 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1053 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
996 yield result
1054 yield result
997
1055
998 @wireprotocommand(
1056 @wireprotocommand(
999 'listkeys',
1057 'listkeys',
1000 args={
1058 args={
1001 'namespace': {
1059 'namespace': {
1002 'type': 'bytes',
1060 'type': 'bytes',
1003 'example': b'ns',
1061 'example': b'ns',
1004 },
1062 },
1005 },
1063 },
1006 permission='pull')
1064 permission='pull')
1007 def listkeysv2(repo, proto, namespace):
1065 def listkeysv2(repo, proto, namespace):
1008 keys = repo.listkeys(encoding.tolocal(namespace))
1066 keys = repo.listkeys(encoding.tolocal(namespace))
1009 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
1067 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
1010 for k, v in keys.iteritems()}
1068 for k, v in keys.iteritems()}
1011
1069
1012 yield keys
1070 yield keys
1013
1071
1014 @wireprotocommand(
1072 @wireprotocommand(
1015 'lookup',
1073 'lookup',
1016 args={
1074 args={
1017 'key': {
1075 'key': {
1018 'type': 'bytes',
1076 'type': 'bytes',
1019 'example': b'foo',
1077 'example': b'foo',
1020 },
1078 },
1021 },
1079 },
1022 permission='pull')
1080 permission='pull')
1023 def lookupv2(repo, proto, key):
1081 def lookupv2(repo, proto, key):
1024 key = encoding.tolocal(key)
1082 key = encoding.tolocal(key)
1025
1083
1026 # TODO handle exception.
1084 # TODO handle exception.
1027 node = repo.lookup(key)
1085 node = repo.lookup(key)
1028
1086
1029 yield node
1087 yield node
1030
1088
1031 @wireprotocommand(
1089 @wireprotocommand(
1032 'manifestdata',
1090 'manifestdata',
1033 args={
1091 args={
1034 'nodes': {
1092 'nodes': {
1035 'type': 'list',
1093 'type': 'list',
1036 'example': [b'0123456...'],
1094 'example': [b'0123456...'],
1037 },
1095 },
1038 'haveparents': {
1096 'haveparents': {
1039 'type': 'bool',
1097 'type': 'bool',
1040 'default': lambda: False,
1098 'default': lambda: False,
1041 'example': True,
1099 'example': True,
1042 },
1100 },
1043 'fields': {
1101 'fields': {
1044 'type': 'set',
1102 'type': 'set',
1045 'default': set,
1103 'default': set,
1046 'example': {b'parents', b'revision'},
1104 'example': {b'parents', b'revision'},
1047 'validvalues': {b'parents', b'revision'},
1105 'validvalues': {b'parents', b'revision'},
1048 },
1106 },
1049 'tree': {
1107 'tree': {
1050 'type': 'bytes',
1108 'type': 'bytes',
1051 'example': b'',
1109 'example': b'',
1052 },
1110 },
1053 },
1111 },
1054 permission='pull',
1112 permission='pull',
1055 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True))
1113 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True))
1056 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1114 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1057 store = repo.manifestlog.getstorage(tree)
1115 store = repo.manifestlog.getstorage(tree)
1058
1116
1059 # Validate the node is known and abort on unknown revisions.
1117 # Validate the node is known and abort on unknown revisions.
1060 for node in nodes:
1118 for node in nodes:
1061 try:
1119 try:
1062 store.rev(node)
1120 store.rev(node)
1063 except error.LookupError:
1121 except error.LookupError:
1064 raise error.WireprotoCommandError(
1122 raise error.WireprotoCommandError(
1065 'unknown node: %s', (node,))
1123 'unknown node: %s', (node,))
1066
1124
1067 revisions = store.emitrevisions(nodes,
1125 revisions = store.emitrevisions(nodes,
1068 revisiondata=b'revision' in fields,
1126 revisiondata=b'revision' in fields,
1069 assumehaveparentrevisions=haveparents)
1127 assumehaveparentrevisions=haveparents)
1070
1128
1071 yield {
1129 yield {
1072 b'totalitems': len(nodes),
1130 b'totalitems': len(nodes),
1073 }
1131 }
1074
1132
1075 for revision in revisions:
1133 for revision in revisions:
1076 d = {
1134 d = {
1077 b'node': revision.node,
1135 b'node': revision.node,
1078 }
1136 }
1079
1137
1080 if b'parents' in fields:
1138 if b'parents' in fields:
1081 d[b'parents'] = [revision.p1node, revision.p2node]
1139 d[b'parents'] = [revision.p1node, revision.p2node]
1082
1140
1083 followingmeta = []
1141 followingmeta = []
1084 followingdata = []
1142 followingdata = []
1085
1143
1086 if b'revision' in fields:
1144 if b'revision' in fields:
1087 if revision.revision is not None:
1145 if revision.revision is not None:
1088 followingmeta.append((b'revision', len(revision.revision)))
1146 followingmeta.append((b'revision', len(revision.revision)))
1089 followingdata.append(revision.revision)
1147 followingdata.append(revision.revision)
1090 else:
1148 else:
1091 d[b'deltabasenode'] = revision.basenode
1149 d[b'deltabasenode'] = revision.basenode
1092 followingmeta.append((b'delta', len(revision.delta)))
1150 followingmeta.append((b'delta', len(revision.delta)))
1093 followingdata.append(revision.delta)
1151 followingdata.append(revision.delta)
1094
1152
1095 if followingmeta:
1153 if followingmeta:
1096 d[b'fieldsfollowing'] = followingmeta
1154 d[b'fieldsfollowing'] = followingmeta
1097
1155
1098 yield d
1156 yield d
1099
1157
1100 for extra in followingdata:
1158 for extra in followingdata:
1101 yield extra
1159 yield extra
1102
1160
1103 @wireprotocommand(
1161 @wireprotocommand(
1104 'pushkey',
1162 'pushkey',
1105 args={
1163 args={
1106 'namespace': {
1164 'namespace': {
1107 'type': 'bytes',
1165 'type': 'bytes',
1108 'example': b'ns',
1166 'example': b'ns',
1109 },
1167 },
1110 'key': {
1168 'key': {
1111 'type': 'bytes',
1169 'type': 'bytes',
1112 'example': b'key',
1170 'example': b'key',
1113 },
1171 },
1114 'old': {
1172 'old': {
1115 'type': 'bytes',
1173 'type': 'bytes',
1116 'example': b'old',
1174 'example': b'old',
1117 },
1175 },
1118 'new': {
1176 'new': {
1119 'type': 'bytes',
1177 'type': 'bytes',
1120 'example': 'new',
1178 'example': 'new',
1121 },
1179 },
1122 },
1180 },
1123 permission='push')
1181 permission='push')
1124 def pushkeyv2(repo, proto, namespace, key, old, new):
1182 def pushkeyv2(repo, proto, namespace, key, old, new):
1125 # TODO handle ui output redirection
1183 # TODO handle ui output redirection
1126 yield repo.pushkey(encoding.tolocal(namespace),
1184 yield repo.pushkey(encoding.tolocal(namespace),
1127 encoding.tolocal(key),
1185 encoding.tolocal(key),
1128 encoding.tolocal(old),
1186 encoding.tolocal(old),
1129 encoding.tolocal(new))
1187 encoding.tolocal(new))
@@ -1,100 +1,118
1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
2 #
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from mercurial import (
10 from mercurial import (
11 extensions,
11 extensions,
12 registrar,
12 registrar,
13 repository,
13 repository,
14 util,
14 util,
15 wireprototypes,
15 wireprototypes,
16 wireprotov2server,
16 wireprotov2server,
17 )
17 )
18 from mercurial.utils import (
18 from mercurial.utils import (
19 interfaceutil,
19 interfaceutil,
20 stringutil,
20 )
21 )
21
22
22 CACHE = None
23 CACHE = None
23
24
24 configtable = {}
25 configtable = {}
25 configitem = registrar.configitem(configtable)
26 configitem = registrar.configitem(configtable)
26
27
27 configitem('simplecache', 'cacheobjects',
28 configitem('simplecache', 'cacheobjects',
28 default=False)
29 default=False)
30 configitem('simplecache', 'redirectsfile',
31 default=None)
29
32
30 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
33 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
31 class memorycacher(object):
34 class memorycacher(object):
32 def __init__(self, ui, command, encodefn):
35 def __init__(self, ui, command, encodefn):
33 self.ui = ui
36 self.ui = ui
34 self.encodefn = encodefn
37 self.encodefn = encodefn
35 self.key = None
38 self.key = None
36 self.cacheobjects = ui.configbool('simplecache', 'cacheobjects')
39 self.cacheobjects = ui.configbool('simplecache', 'cacheobjects')
37 self.buffered = []
40 self.buffered = []
38
41
39 ui.log('simplecache', 'cacher constructed for %s\n', command)
42 ui.log('simplecache', 'cacher constructed for %s\n', command)
40
43
41 def __enter__(self):
44 def __enter__(self):
42 return self
45 return self
43
46
44 def __exit__(self, exctype, excvalue, exctb):
47 def __exit__(self, exctype, excvalue, exctb):
45 if exctype:
48 if exctype:
46 self.ui.log('simplecache', 'cacher exiting due to error\n')
49 self.ui.log('simplecache', 'cacher exiting due to error\n')
47
50
48 def adjustcachekeystate(self, state):
51 def adjustcachekeystate(self, state):
49 # Needed in order to make tests deterministic. Don't copy this
52 # Needed in order to make tests deterministic. Don't copy this
50 # pattern for production caches!
53 # pattern for production caches!
51 del state[b'repo']
54 del state[b'repo']
52
55
53 def setcachekey(self, key):
56 def setcachekey(self, key):
54 self.key = key
57 self.key = key
55 return True
58 return True
56
59
57 def lookup(self):
60 def lookup(self):
58 if self.key not in CACHE:
61 if self.key not in CACHE:
59 self.ui.log('simplecache', 'cache miss for %s\n', self.key)
62 self.ui.log('simplecache', 'cache miss for %s\n', self.key)
60 return None
63 return None
61
64
62 entry = CACHE[self.key]
65 entry = CACHE[self.key]
63 self.ui.log('simplecache', 'cache hit for %s\n', self.key)
66 self.ui.log('simplecache', 'cache hit for %s\n', self.key)
64
67
65 if self.cacheobjects:
68 if self.cacheobjects:
66 return {
69 return {
67 'objs': entry,
70 'objs': entry,
68 }
71 }
69 else:
72 else:
70 return {
73 return {
71 'objs': [wireprototypes.encodedresponse(entry)],
74 'objs': [wireprototypes.encodedresponse(entry)],
72 }
75 }
73
76
74 def onobject(self, obj):
77 def onobject(self, obj):
75 if self.cacheobjects:
78 if self.cacheobjects:
76 self.buffered.append(obj)
79 self.buffered.append(obj)
77 else:
80 else:
78 self.buffered.extend(self.encodefn(obj))
81 self.buffered.extend(self.encodefn(obj))
79
82
80 yield obj
83 yield obj
81
84
82 def onfinished(self):
85 def onfinished(self):
83 self.ui.log('simplecache', 'storing cache entry for %s\n', self.key)
86 self.ui.log('simplecache', 'storing cache entry for %s\n', self.key)
84 if self.cacheobjects:
87 if self.cacheobjects:
85 CACHE[self.key] = self.buffered
88 CACHE[self.key] = self.buffered
86 else:
89 else:
87 CACHE[self.key] = b''.join(self.buffered)
90 CACHE[self.key] = b''.join(self.buffered)
88
91
89 return []
92 return []
90
93
91 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn):
94 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn):
92 return memorycacher(repo.ui, command, objencoderfn)
95 return memorycacher(repo.ui, command, objencoderfn)
93
96
97 def loadredirecttargets(ui):
98 path = ui.config('simplecache', 'redirectsfile')
99 if not path:
100 return []
101
102 with open(path, 'rb') as fh:
103 s = fh.read()
104
105 return stringutil.evalpythonliteral(s)
106
107 def getadvertisedredirecttargets(orig, repo, proto):
108 return loadredirecttargets(repo.ui)
109
94 def extsetup(ui):
110 def extsetup(ui):
95 global CACHE
111 global CACHE
96
112
97 CACHE = util.lrucachedict(10000)
113 CACHE = util.lrucachedict(10000)
98
114
99 extensions.wrapfunction(wireprotov2server, 'makeresponsecacher',
115 extensions.wrapfunction(wireprotov2server, 'makeresponsecacher',
100 makeresponsecacher)
116 makeresponsecacher)
117 extensions.wrapfunction(wireprotov2server, 'getadvertisedredirecttargets',
118 getadvertisedredirecttargets)
General Comments 0
You need to be logged in to leave comments. Login now