##// END OF EJS Templates
pytype: stop excluding wireprotov2server.py...
Matt Harbison -
r49311:cfb4f1de default
parent child Browse files
Show More
@@ -1,1613 +1,1617 b''
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 Olivia Mackall <olivia@selenic.com>
2 # Copyright 2005-2007 Olivia Mackall <olivia@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 collections
9 import collections
10 import contextlib
10 import contextlib
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import hex
13 from .node import hex
14 from . import (
14 from . import (
15 discovery,
15 discovery,
16 encoding,
16 encoding,
17 error,
17 error,
18 match as matchmod,
18 match as matchmod,
19 narrowspec,
19 narrowspec,
20 pycompat,
20 pycompat,
21 streamclone,
21 streamclone,
22 templatefilters,
22 templatefilters,
23 util,
23 util,
24 wireprotoframing,
24 wireprotoframing,
25 wireprototypes,
25 wireprototypes,
26 )
26 )
27 from .interfaces import util as interfaceutil
27 from .interfaces import util as interfaceutil
28 from .utils import (
28 from .utils import (
29 cborutil,
29 cborutil,
30 hashutil,
30 hashutil,
31 stringutil,
31 stringutil,
32 )
32 )
33
33
34 FRAMINGTYPE = b'application/mercurial-exp-framing-0006'
34 FRAMINGTYPE = b'application/mercurial-exp-framing-0006'
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
45
46 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
46 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
47 from .hgweb import common as hgwebcommon
47 from .hgweb import common as hgwebcommon
48
48
49 # URL space looks like: <permissions>/<command>, where <permission> can
49 # URL space looks like: <permissions>/<command>, where <permission> can
50 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
50 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
51
51
52 # Root URL does nothing meaningful... yet.
52 # Root URL does nothing meaningful... yet.
53 if not urlparts:
53 if not urlparts:
54 res.status = b'200 OK'
54 res.status = b'200 OK'
55 res.headers[b'Content-Type'] = b'text/plain'
55 res.headers[b'Content-Type'] = b'text/plain'
56 res.setbodybytes(_(b'HTTP version 2 API handler'))
56 res.setbodybytes(_(b'HTTP version 2 API handler'))
57 return
57 return
58
58
59 if len(urlparts) == 1:
59 if len(urlparts) == 1:
60 res.status = b'404 Not Found'
60 res.status = b'404 Not Found'
61 res.headers[b'Content-Type'] = b'text/plain'
61 res.headers[b'Content-Type'] = b'text/plain'
62 res.setbodybytes(
62 res.setbodybytes(
63 _(b'do not know how to process %s\n') % req.dispatchpath
63 _(b'do not know how to process %s\n') % req.dispatchpath
64 )
64 )
65 return
65 return
66
66
67 permission, command = urlparts[0:2]
67 permission, command = urlparts[0:2]
68
68
69 if permission not in (b'ro', b'rw'):
69 if permission not in (b'ro', b'rw'):
70 res.status = b'404 Not Found'
70 res.status = b'404 Not Found'
71 res.headers[b'Content-Type'] = b'text/plain'
71 res.headers[b'Content-Type'] = b'text/plain'
72 res.setbodybytes(_(b'unknown permission: %s') % permission)
72 res.setbodybytes(_(b'unknown permission: %s') % permission)
73 return
73 return
74
74
75 if req.method != b'POST':
75 if req.method != b'POST':
76 res.status = b'405 Method Not Allowed'
76 res.status = b'405 Method Not Allowed'
77 res.headers[b'Allow'] = b'POST'
77 res.headers[b'Allow'] = b'POST'
78 res.setbodybytes(_(b'commands require POST requests'))
78 res.setbodybytes(_(b'commands require POST requests'))
79 return
79 return
80
80
81 # At some point we'll want to use our own API instead of recycling the
81 # At some point we'll want to use our own API instead of recycling the
82 # behavior of version 1 of the wire protocol...
82 # behavior of version 1 of the wire protocol...
83 # TODO return reasonable responses - not responses that overload the
83 # TODO return reasonable responses - not responses that overload the
84 # HTTP status line message for error reporting.
84 # HTTP status line message for error reporting.
85 try:
85 try:
86 checkperm(rctx, req, b'pull' if permission == b'ro' else b'push')
86 checkperm(rctx, req, b'pull' if permission == b'ro' else b'push')
87 except hgwebcommon.ErrorResponse as e:
87 except hgwebcommon.ErrorResponse as e:
88 res.status = hgwebcommon.statusmessage(
88 res.status = hgwebcommon.statusmessage(
89 e.code, stringutil.forcebytestr(e)
89 e.code, stringutil.forcebytestr(e)
90 )
90 )
91 for k, v in e.headers:
91 for k, v in e.headers:
92 res.headers[k] = v
92 res.headers[k] = v
93 res.setbodybytes(b'permission denied')
93 res.setbodybytes(b'permission denied')
94 return
94 return
95
95
96 # We have a special endpoint to reflect the request back at the client.
96 # We have a special endpoint to reflect the request back at the client.
97 if command == b'debugreflect':
97 if command == b'debugreflect':
98 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
98 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
99 return
99 return
100
100
101 # Extra commands that we handle that aren't really wire protocol
101 # Extra commands that we handle that aren't really wire protocol
102 # commands. Think extra hard before making this hackery available to
102 # commands. Think extra hard before making this hackery available to
103 # extension.
103 # extension.
104 extracommands = {b'multirequest'}
104 extracommands = {b'multirequest'}
105
105
106 if command not in COMMANDS and command not in extracommands:
106 if command not in COMMANDS and command not in extracommands:
107 res.status = b'404 Not Found'
107 res.status = b'404 Not Found'
108 res.headers[b'Content-Type'] = b'text/plain'
108 res.headers[b'Content-Type'] = b'text/plain'
109 res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command)
109 res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command)
110 return
110 return
111
111
112 repo = rctx.repo
112 repo = rctx.repo
113 ui = repo.ui
113 ui = repo.ui
114
114
115 proto = httpv2protocolhandler(req, ui)
115 proto = httpv2protocolhandler(req, ui)
116
116
117 if (
117 if (
118 not COMMANDS.commandavailable(command, proto)
118 not COMMANDS.commandavailable(command, proto)
119 and command not in extracommands
119 and command not in extracommands
120 ):
120 ):
121 res.status = b'404 Not Found'
121 res.status = b'404 Not Found'
122 res.headers[b'Content-Type'] = b'text/plain'
122 res.headers[b'Content-Type'] = b'text/plain'
123 res.setbodybytes(_(b'invalid wire protocol command: %s') % command)
123 res.setbodybytes(_(b'invalid wire protocol command: %s') % command)
124 return
124 return
125
125
126 # TODO consider cases where proxies may add additional Accept headers.
126 # TODO consider cases where proxies may add additional Accept headers.
127 if req.headers.get(b'Accept') != FRAMINGTYPE:
127 if req.headers.get(b'Accept') != FRAMINGTYPE:
128 res.status = b'406 Not Acceptable'
128 res.status = b'406 Not Acceptable'
129 res.headers[b'Content-Type'] = b'text/plain'
129 res.headers[b'Content-Type'] = b'text/plain'
130 res.setbodybytes(
130 res.setbodybytes(
131 _(b'client MUST specify Accept header with value: %s\n')
131 _(b'client MUST specify Accept header with value: %s\n')
132 % FRAMINGTYPE
132 % FRAMINGTYPE
133 )
133 )
134 return
134 return
135
135
136 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
136 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
137 res.status = b'415 Unsupported Media Type'
137 res.status = b'415 Unsupported Media Type'
138 # TODO we should send a response with appropriate media type,
138 # TODO we should send a response with appropriate media type,
139 # since client does Accept it.
139 # since client does Accept it.
140 res.headers[b'Content-Type'] = b'text/plain'
140 res.headers[b'Content-Type'] = b'text/plain'
141 res.setbodybytes(
141 res.setbodybytes(
142 _(b'client MUST send Content-Type header with value: %s\n')
142 _(b'client MUST send Content-Type header with value: %s\n')
143 % FRAMINGTYPE
143 % FRAMINGTYPE
144 )
144 )
145 return
145 return
146
146
147 _processhttpv2request(ui, repo, req, res, permission, command, proto)
147 _processhttpv2request(ui, repo, req, res, permission, command, proto)
148
148
149
149
150 def _processhttpv2reflectrequest(ui, repo, req, res):
150 def _processhttpv2reflectrequest(ui, repo, req, res):
151 """Reads unified frame protocol request and dumps out state to client.
151 """Reads unified frame protocol request and dumps out state to client.
152
152
153 This special endpoint can be used to help debug the wire protocol.
153 This special endpoint can be used to help debug the wire protocol.
154
154
155 Instead of routing the request through the normal dispatch mechanism,
155 Instead of routing the request through the normal dispatch mechanism,
156 we instead read all frames, decode them, and feed them into our state
156 we instead read all frames, decode them, and feed them into our state
157 tracker. We then dump the log of all that activity back out to the
157 tracker. We then dump the log of all that activity back out to the
158 client.
158 client.
159 """
159 """
160 # Reflection APIs have a history of being abused, accidentally disclosing
160 # Reflection APIs have a history of being abused, accidentally disclosing
161 # sensitive data, etc. So we have a config knob.
161 # sensitive data, etc. So we have a config knob.
162 if not ui.configbool(b'experimental', b'web.api.debugreflect'):
162 if not ui.configbool(b'experimental', b'web.api.debugreflect'):
163 res.status = b'404 Not Found'
163 res.status = b'404 Not Found'
164 res.headers[b'Content-Type'] = b'text/plain'
164 res.headers[b'Content-Type'] = b'text/plain'
165 res.setbodybytes(_(b'debugreflect service not available'))
165 res.setbodybytes(_(b'debugreflect service not available'))
166 return
166 return
167
167
168 # We assume we have a unified framing protocol request body.
168 # We assume we have a unified framing protocol request body.
169
169
170 reactor = wireprotoframing.serverreactor(ui)
170 reactor = wireprotoframing.serverreactor(ui)
171 states = []
171 states = []
172
172
173 while True:
173 while True:
174 frame = wireprotoframing.readframe(req.bodyfh)
174 frame = wireprotoframing.readframe(req.bodyfh)
175
175
176 if not frame:
176 if not frame:
177 states.append(b'received: <no frame>')
177 states.append(b'received: <no frame>')
178 break
178 break
179
179
180 states.append(
180 states.append(
181 b'received: %d %d %d %s'
181 b'received: %d %d %d %s'
182 % (frame.typeid, frame.flags, frame.requestid, frame.payload)
182 % (frame.typeid, frame.flags, frame.requestid, frame.payload)
183 )
183 )
184
184
185 action, meta = reactor.onframerecv(frame)
185 action, meta = reactor.onframerecv(frame)
186 states.append(templatefilters.json((action, meta)))
186 states.append(templatefilters.json((action, meta)))
187
187
188 action, meta = reactor.oninputeof()
188 action, meta = reactor.oninputeof()
189 meta[b'action'] = action
189 meta[b'action'] = action
190 states.append(templatefilters.json(meta))
190 states.append(templatefilters.json(meta))
191
191
192 res.status = b'200 OK'
192 res.status = b'200 OK'
193 res.headers[b'Content-Type'] = b'text/plain'
193 res.headers[b'Content-Type'] = b'text/plain'
194 res.setbodybytes(b'\n'.join(states))
194 res.setbodybytes(b'\n'.join(states))
195
195
196
196
197 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
197 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
198 """Post-validation handler for HTTPv2 requests.
198 """Post-validation handler for HTTPv2 requests.
199
199
200 Called when the HTTP request contains unified frame-based protocol
200 Called when the HTTP request contains unified frame-based protocol
201 frames for evaluation.
201 frames for evaluation.
202 """
202 """
203 # TODO Some HTTP clients are full duplex and can receive data before
203 # TODO Some HTTP clients are full duplex and can receive data before
204 # the entire request is transmitted. Figure out a way to indicate support
204 # the entire request is transmitted. Figure out a way to indicate support
205 # for that so we can opt into full duplex mode.
205 # for that so we can opt into full duplex mode.
206 reactor = wireprotoframing.serverreactor(ui, deferoutput=True)
206 reactor = wireprotoframing.serverreactor(ui, deferoutput=True)
207 seencommand = False
207 seencommand = False
208
208
209 outstream = None
209 outstream = None
210
210
211 while True:
211 while True:
212 frame = wireprotoframing.readframe(req.bodyfh)
212 frame = wireprotoframing.readframe(req.bodyfh)
213 if not frame:
213 if not frame:
214 break
214 break
215
215
216 action, meta = reactor.onframerecv(frame)
216 action, meta = reactor.onframerecv(frame)
217
217
218 if action == b'wantframe':
218 if action == b'wantframe':
219 # Need more data before we can do anything.
219 # Need more data before we can do anything.
220 continue
220 continue
221 elif action == b'runcommand':
221 elif action == b'runcommand':
222 # Defer creating output stream because we need to wait for
222 # Defer creating output stream because we need to wait for
223 # protocol settings frames so proper encoding can be applied.
223 # protocol settings frames so proper encoding can be applied.
224 if not outstream:
224 if not outstream:
225 outstream = reactor.makeoutputstream()
225 outstream = reactor.makeoutputstream()
226
226
227 sentoutput = _httpv2runcommand(
227 sentoutput = _httpv2runcommand(
228 ui,
228 ui,
229 repo,
229 repo,
230 req,
230 req,
231 res,
231 res,
232 authedperm,
232 authedperm,
233 reqcommand,
233 reqcommand,
234 reactor,
234 reactor,
235 outstream,
235 outstream,
236 meta,
236 meta,
237 issubsequent=seencommand,
237 issubsequent=seencommand,
238 )
238 )
239
239
240 if sentoutput:
240 if sentoutput:
241 return
241 return
242
242
243 seencommand = True
243 seencommand = True
244
244
245 elif action == b'error':
245 elif action == b'error':
246 # TODO define proper error mechanism.
246 # TODO define proper error mechanism.
247 res.status = b'200 OK'
247 res.status = b'200 OK'
248 res.headers[b'Content-Type'] = b'text/plain'
248 res.headers[b'Content-Type'] = b'text/plain'
249 res.setbodybytes(meta[b'message'] + b'\n')
249 res.setbodybytes(meta[b'message'] + b'\n')
250 return
250 return
251 else:
251 else:
252 raise error.ProgrammingError(
252 raise error.ProgrammingError(
253 b'unhandled action from frame processor: %s' % action
253 b'unhandled action from frame processor: %s' % action
254 )
254 )
255
255
256 action, meta = reactor.oninputeof()
256 action, meta = reactor.oninputeof()
257 if action == b'sendframes':
257 if action == b'sendframes':
258 # We assume we haven't started sending the response yet. If we're
258 # We assume we haven't started sending the response yet. If we're
259 # wrong, the response type will raise an exception.
259 # wrong, the response type will raise an exception.
260 res.status = b'200 OK'
260 res.status = b'200 OK'
261 res.headers[b'Content-Type'] = FRAMINGTYPE
261 res.headers[b'Content-Type'] = FRAMINGTYPE
262 res.setbodygen(meta[b'framegen'])
262 res.setbodygen(meta[b'framegen'])
263 elif action == b'noop':
263 elif action == b'noop':
264 pass
264 pass
265 else:
265 else:
266 raise error.ProgrammingError(
266 raise error.ProgrammingError(
267 b'unhandled action from frame processor: %s' % action
267 b'unhandled action from frame processor: %s' % action
268 )
268 )
269
269
270
270
271 def _httpv2runcommand(
271 def _httpv2runcommand(
272 ui,
272 ui,
273 repo,
273 repo,
274 req,
274 req,
275 res,
275 res,
276 authedperm,
276 authedperm,
277 reqcommand,
277 reqcommand,
278 reactor,
278 reactor,
279 outstream,
279 outstream,
280 command,
280 command,
281 issubsequent,
281 issubsequent,
282 ):
282 ):
283 """Dispatch a wire protocol command made from HTTPv2 requests.
283 """Dispatch a wire protocol command made from HTTPv2 requests.
284
284
285 The authenticated permission (``authedperm``) along with the original
285 The authenticated permission (``authedperm``) along with the original
286 command from the URL (``reqcommand``) are passed in.
286 command from the URL (``reqcommand``) are passed in.
287 """
287 """
288 # We already validated that the session has permissions to perform the
288 # We already validated that the session has permissions to perform the
289 # actions in ``authedperm``. In the unified frame protocol, the canonical
289 # actions in ``authedperm``. In the unified frame protocol, the canonical
290 # command to run is expressed in a frame. However, the URL also requested
290 # command to run is expressed in a frame. However, the URL also requested
291 # to run a specific command. We need to be careful that the command we
291 # to run a specific command. We need to be careful that the command we
292 # run doesn't have permissions requirements greater than what was granted
292 # run doesn't have permissions requirements greater than what was granted
293 # by ``authedperm``.
293 # by ``authedperm``.
294 #
294 #
295 # Our rule for this is we only allow one command per HTTP request and
295 # Our rule for this is we only allow one command per HTTP request and
296 # that command must match the command in the URL. However, we make
296 # that command must match the command in the URL. However, we make
297 # an exception for the ``multirequest`` URL. This URL is allowed to
297 # an exception for the ``multirequest`` URL. This URL is allowed to
298 # execute multiple commands. We double check permissions of each command
298 # execute multiple commands. We double check permissions of each command
299 # as it is invoked to ensure there is no privilege escalation.
299 # as it is invoked to ensure there is no privilege escalation.
300 # TODO consider allowing multiple commands to regular command URLs
300 # TODO consider allowing multiple commands to regular command URLs
301 # iff each command is the same.
301 # iff each command is the same.
302
302
303 proto = httpv2protocolhandler(req, ui, args=command[b'args'])
303 proto = httpv2protocolhandler(req, ui, args=command[b'args'])
304
304
305 if reqcommand == b'multirequest':
305 if reqcommand == b'multirequest':
306 if not COMMANDS.commandavailable(command[b'command'], proto):
306 if not COMMANDS.commandavailable(command[b'command'], proto):
307 # TODO proper error mechanism
307 # TODO proper error mechanism
308 res.status = b'200 OK'
308 res.status = b'200 OK'
309 res.headers[b'Content-Type'] = b'text/plain'
309 res.headers[b'Content-Type'] = b'text/plain'
310 res.setbodybytes(
310 res.setbodybytes(
311 _(b'wire protocol command not available: %s')
311 _(b'wire protocol command not available: %s')
312 % command[b'command']
312 % command[b'command']
313 )
313 )
314 return True
314 return True
315
315
316 # TODO don't use assert here, since it may be elided by -O.
316 # TODO don't use assert here, since it may be elided by -O.
317 assert authedperm in (b'ro', b'rw')
317 assert authedperm in (b'ro', b'rw')
318 wirecommand = COMMANDS[command[b'command']]
318 wirecommand = COMMANDS[command[b'command']]
319 assert wirecommand.permission in (b'push', b'pull')
319 assert wirecommand.permission in (b'push', b'pull')
320
320
321 if authedperm == b'ro' and wirecommand.permission != b'pull':
321 if authedperm == b'ro' and wirecommand.permission != b'pull':
322 # TODO proper error mechanism
322 # TODO proper error mechanism
323 res.status = b'403 Forbidden'
323 res.status = b'403 Forbidden'
324 res.headers[b'Content-Type'] = b'text/plain'
324 res.headers[b'Content-Type'] = b'text/plain'
325 res.setbodybytes(
325 res.setbodybytes(
326 _(b'insufficient permissions to execute command: %s')
326 _(b'insufficient permissions to execute command: %s')
327 % command[b'command']
327 % command[b'command']
328 )
328 )
329 return True
329 return True
330
330
331 # TODO should we also call checkperm() here? Maybe not if we're going
331 # TODO should we also call checkperm() here? Maybe not if we're going
332 # to overhaul that API. The granted scope from the URL check should
332 # to overhaul that API. The granted scope from the URL check should
333 # be good enough.
333 # be good enough.
334
334
335 else:
335 else:
336 # Don't allow multiple commands outside of ``multirequest`` URL.
336 # Don't allow multiple commands outside of ``multirequest`` URL.
337 if issubsequent:
337 if issubsequent:
338 # TODO proper error mechanism
338 # TODO proper error mechanism
339 res.status = b'200 OK'
339 res.status = b'200 OK'
340 res.headers[b'Content-Type'] = b'text/plain'
340 res.headers[b'Content-Type'] = b'text/plain'
341 res.setbodybytes(
341 res.setbodybytes(
342 _(b'multiple commands cannot be issued to this URL')
342 _(b'multiple commands cannot be issued to this URL')
343 )
343 )
344 return True
344 return True
345
345
346 if reqcommand != command[b'command']:
346 if reqcommand != command[b'command']:
347 # TODO define proper error mechanism
347 # TODO define proper error mechanism
348 res.status = b'200 OK'
348 res.status = b'200 OK'
349 res.headers[b'Content-Type'] = b'text/plain'
349 res.headers[b'Content-Type'] = b'text/plain'
350 res.setbodybytes(_(b'command in frame must match command in URL'))
350 res.setbodybytes(_(b'command in frame must match command in URL'))
351 return True
351 return True
352
352
353 res.status = b'200 OK'
353 res.status = b'200 OK'
354 res.headers[b'Content-Type'] = FRAMINGTYPE
354 res.headers[b'Content-Type'] = FRAMINGTYPE
355
355
356 try:
356 try:
357 objs = dispatch(repo, proto, command[b'command'], command[b'redirect'])
357 objs = dispatch(repo, proto, command[b'command'], command[b'redirect'])
358
358
359 action, meta = reactor.oncommandresponsereadyobjects(
359 action, meta = reactor.oncommandresponsereadyobjects(
360 outstream, command[b'requestid'], objs
360 outstream, command[b'requestid'], objs
361 )
361 )
362
362
363 except error.WireprotoCommandError as e:
363 except error.WireprotoCommandError as e:
364 action, meta = reactor.oncommanderror(
364 action, meta = reactor.oncommanderror(
365 outstream, command[b'requestid'], e.message, e.messageargs
365 outstream, command[b'requestid'], e.message, e.messageargs
366 )
366 )
367
367
368 except Exception as e:
368 except Exception as e:
369 action, meta = reactor.onservererror(
369 action, meta = reactor.onservererror(
370 outstream,
370 outstream,
371 command[b'requestid'],
371 command[b'requestid'],
372 _(b'exception when invoking command: %s')
372 _(b'exception when invoking command: %s')
373 % stringutil.forcebytestr(e),
373 % stringutil.forcebytestr(e),
374 )
374 )
375
375
376 if action == b'sendframes':
376 if action == b'sendframes':
377 res.setbodygen(meta[b'framegen'])
377 res.setbodygen(meta[b'framegen'])
378 return True
378 return True
379 elif action == b'noop':
379 elif action == b'noop':
380 return False
380 return False
381 else:
381 else:
382 raise error.ProgrammingError(
382 raise error.ProgrammingError(
383 b'unhandled event from reactor: %s' % action
383 b'unhandled event from reactor: %s' % action
384 )
384 )
385
385
386
386
387 def getdispatchrepo(repo, proto, command):
387 def getdispatchrepo(repo, proto, command):
388 viewconfig = repo.ui.config(b'server', b'view')
388 viewconfig = repo.ui.config(b'server', b'view')
389 return repo.filtered(viewconfig)
389 return repo.filtered(viewconfig)
390
390
391
391
392 def dispatch(repo, proto, command, redirect):
392 def dispatch(repo, proto, command, redirect):
393 """Run a wire protocol command.
393 """Run a wire protocol command.
394
394
395 Returns an iterable of objects that will be sent to the client.
395 Returns an iterable of objects that will be sent to the client.
396 """
396 """
397 repo = getdispatchrepo(repo, proto, command)
397 repo = getdispatchrepo(repo, proto, command)
398
398
399 entry = COMMANDS[command]
399 entry = COMMANDS[command]
400 func = entry.func
400 func = entry.func
401 spec = entry.args
401 spec = entry.args
402
402
403 args = proto.getargs(spec)
403 args = proto.getargs(spec)
404
404
405 # There is some duplicate boilerplate code here for calling the command and
405 # There is some duplicate boilerplate code here for calling the command and
406 # emitting objects. It is either that or a lot of indented code that looks
406 # emitting objects. It is either that or a lot of indented code that looks
407 # like a pyramid (since there are a lot of code paths that result in not
407 # like a pyramid (since there are a lot of code paths that result in not
408 # using the cacher).
408 # using the cacher).
409 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
409 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
410
410
411 # Request is not cacheable. Don't bother instantiating a cacher.
411 # Request is not cacheable. Don't bother instantiating a cacher.
412 if not entry.cachekeyfn:
412 if not entry.cachekeyfn:
413 for o in callcommand():
413 for o in callcommand():
414 yield o
414 yield o
415 return
415 return
416
416
417 if redirect:
417 if redirect:
418 redirecttargets = redirect[b'targets']
418 redirecttargets = redirect[b'targets']
419 redirecthashes = redirect[b'hashes']
419 redirecthashes = redirect[b'hashes']
420 else:
420 else:
421 redirecttargets = []
421 redirecttargets = []
422 redirecthashes = []
422 redirecthashes = []
423
423
424 cacher = makeresponsecacher(
424 cacher = makeresponsecacher(
425 repo,
425 repo,
426 proto,
426 proto,
427 command,
427 command,
428 args,
428 args,
429 cborutil.streamencode,
429 cborutil.streamencode,
430 redirecttargets=redirecttargets,
430 redirecttargets=redirecttargets,
431 redirecthashes=redirecthashes,
431 redirecthashes=redirecthashes,
432 )
432 )
433
433
434 # But we have no cacher. Do default handling.
434 # But we have no cacher. Do default handling.
435 if not cacher:
435 if not cacher:
436 for o in callcommand():
436 for o in callcommand():
437 yield o
437 yield o
438 return
438 return
439
439
440 with cacher:
440 with cacher:
441 cachekey = entry.cachekeyfn(
441 cachekey = entry.cachekeyfn(
442 repo, proto, cacher, **pycompat.strkwargs(args)
442 repo, proto, cacher, **pycompat.strkwargs(args)
443 )
443 )
444
444
445 # No cache key or the cacher doesn't like it. Do default handling.
445 # No cache key or the cacher doesn't like it. Do default handling.
446 if cachekey is None or not cacher.setcachekey(cachekey):
446 if cachekey is None or not cacher.setcachekey(cachekey):
447 for o in callcommand():
447 for o in callcommand():
448 yield o
448 yield o
449 return
449 return
450
450
451 # Serve it from the cache, if possible.
451 # Serve it from the cache, if possible.
452 cached = cacher.lookup()
452 cached = cacher.lookup()
453
453
454 if cached:
454 if cached:
455 for o in cached[b'objs']:
455 for o in cached[b'objs']:
456 yield o
456 yield o
457 return
457 return
458
458
459 # Else call the command and feed its output into the cacher, allowing
459 # Else call the command and feed its output into the cacher, allowing
460 # the cacher to buffer/mutate objects as it desires.
460 # the cacher to buffer/mutate objects as it desires.
461 for o in callcommand():
461 for o in callcommand():
462 for o in cacher.onobject(o):
462 for o in cacher.onobject(o):
463 yield o
463 yield o
464
464
465 for o in cacher.onfinished():
465 for o in cacher.onfinished():
466 yield o
466 yield o
467
467
468
468
469 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
469 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
470 class httpv2protocolhandler(object):
470 class httpv2protocolhandler(object):
471 def __init__(self, req, ui, args=None):
471 def __init__(self, req, ui, args=None):
472 self._req = req
472 self._req = req
473 self._ui = ui
473 self._ui = ui
474 self._args = args
474 self._args = args
475
475
476 @property
476 @property
477 def name(self):
477 def name(self):
478 return HTTP_WIREPROTO_V2
478 return HTTP_WIREPROTO_V2
479
479
480 def getargs(self, args):
480 def getargs(self, args):
481 # First look for args that were passed but aren't registered on this
481 # First look for args that were passed but aren't registered on this
482 # command.
482 # command.
483 extra = set(self._args) - set(args)
483 extra = set(self._args) - set(args)
484 if extra:
484 if extra:
485 raise error.WireprotoCommandError(
485 raise error.WireprotoCommandError(
486 b'unsupported argument to command: %s'
486 b'unsupported argument to command: %s'
487 % b', '.join(sorted(extra))
487 % b', '.join(sorted(extra))
488 )
488 )
489
489
490 # And look for required arguments that are missing.
490 # And look for required arguments that are missing.
491 missing = {a for a in args if args[a][b'required']} - set(self._args)
491 missing = {a for a in args if args[a][b'required']} - set(self._args)
492
492
493 if missing:
493 if missing:
494 raise error.WireprotoCommandError(
494 raise error.WireprotoCommandError(
495 b'missing required arguments: %s' % b', '.join(sorted(missing))
495 b'missing required arguments: %s' % b', '.join(sorted(missing))
496 )
496 )
497
497
498 # Now derive the arguments to pass to the command, taking into
498 # Now derive the arguments to pass to the command, taking into
499 # account the arguments specified by the client.
499 # account the arguments specified by the client.
500 data = {}
500 data = {}
501 for k, meta in sorted(args.items()):
501 for k, meta in sorted(args.items()):
502 # This argument wasn't passed by the client.
502 # This argument wasn't passed by the client.
503 if k not in self._args:
503 if k not in self._args:
504 data[k] = meta[b'default']()
504 data[k] = meta[b'default']()
505 continue
505 continue
506
506
507 v = self._args[k]
507 v = self._args[k]
508
508
509 # Sets may be expressed as lists. Silently normalize.
509 # Sets may be expressed as lists. Silently normalize.
510 if meta[b'type'] == b'set' and isinstance(v, list):
510 if meta[b'type'] == b'set' and isinstance(v, list):
511 v = set(v)
511 v = set(v)
512
512
513 # TODO consider more/stronger type validation.
513 # TODO consider more/stronger type validation.
514
514
515 data[k] = v
515 data[k] = v
516
516
517 return data
517 return data
518
518
519 def getprotocaps(self):
519 def getprotocaps(self):
520 # Protocol capabilities are currently not implemented for HTTP V2.
520 # Protocol capabilities are currently not implemented for HTTP V2.
521 return set()
521 return set()
522
522
523 def getpayload(self):
523 def getpayload(self):
524 raise NotImplementedError
524 raise NotImplementedError
525
525
526 @contextlib.contextmanager
526 @contextlib.contextmanager
527 def mayberedirectstdio(self):
527 def mayberedirectstdio(self):
528 raise NotImplementedError
528 raise NotImplementedError
529
529
530 def client(self):
530 def client(self):
531 raise NotImplementedError
531 raise NotImplementedError
532
532
533 def addcapabilities(self, repo, caps):
533 def addcapabilities(self, repo, caps):
534 return caps
534 return caps
535
535
536 def checkperm(self, perm):
536 def checkperm(self, perm):
537 raise NotImplementedError
537 raise NotImplementedError
538
538
539
539
540 def httpv2apidescriptor(req, repo):
540 def httpv2apidescriptor(req, repo):
541 proto = httpv2protocolhandler(req, repo.ui)
541 proto = httpv2protocolhandler(req, repo.ui)
542
542
543 return _capabilitiesv2(repo, proto)
543 return _capabilitiesv2(repo, proto)
544
544
545
545
546 def _capabilitiesv2(repo, proto):
546 def _capabilitiesv2(repo, proto):
547 """Obtain the set of capabilities for version 2 transports.
547 """Obtain the set of capabilities for version 2 transports.
548
548
549 These capabilities are distinct from the capabilities for version 1
549 These capabilities are distinct from the capabilities for version 1
550 transports.
550 transports.
551 """
551 """
552 caps = {
552 caps = {
553 b'commands': {},
553 b'commands': {},
554 b'framingmediatypes': [FRAMINGTYPE],
554 b'framingmediatypes': [FRAMINGTYPE],
555 b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
555 b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
556 }
556 }
557
557
558 for command, entry in COMMANDS.items():
558 for command, entry in COMMANDS.items():
559 args = {}
559 args = {}
560
560
561 for arg, meta in entry.args.items():
561 for arg, meta in entry.args.items():
562 args[arg] = {
562 args[arg] = {
563 # TODO should this be a normalized type using CBOR's
563 # TODO should this be a normalized type using CBOR's
564 # terminology?
564 # terminology?
565 b'type': meta[b'type'],
565 b'type': meta[b'type'],
566 b'required': meta[b'required'],
566 b'required': meta[b'required'],
567 }
567 }
568
568
569 if not meta[b'required']:
569 if not meta[b'required']:
570 args[arg][b'default'] = meta[b'default']()
570 args[arg][b'default'] = meta[b'default']()
571
571
572 if meta[b'validvalues']:
572 if meta[b'validvalues']:
573 args[arg][b'validvalues'] = meta[b'validvalues']
573 args[arg][b'validvalues'] = meta[b'validvalues']
574
574
575 # TODO this type of check should be defined in a per-command callback.
575 # TODO this type of check should be defined in a per-command callback.
576 if (
576 if (
577 command == b'rawstorefiledata'
577 command == b'rawstorefiledata'
578 and not streamclone.allowservergeneration(repo)
578 and not streamclone.allowservergeneration(repo)
579 ):
579 ):
580 continue
580 continue
581
581
582 # pytype: disable=unsupported-operands
582 caps[b'commands'][command] = {
583 caps[b'commands'][command] = {
583 b'args': args,
584 b'args': args,
584 b'permissions': [entry.permission],
585 b'permissions': [entry.permission],
585 }
586 }
587 # pytype: enable=unsupported-operands
586
588
587 if entry.extracapabilitiesfn:
589 if entry.extracapabilitiesfn:
588 extracaps = entry.extracapabilitiesfn(repo, proto)
590 extracaps = entry.extracapabilitiesfn(repo, proto)
589 caps[b'commands'][command].update(extracaps)
591 caps[b'commands'][command].update(extracaps)
590
592
591 caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
593 caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
592
594
593 targets = getadvertisedredirecttargets(repo, proto)
595 targets = getadvertisedredirecttargets(repo, proto)
594 if targets:
596 if targets:
595 caps[b'redirect'] = {
597 caps[b'redirect'] = {
596 b'targets': [],
598 b'targets': [],
597 b'hashes': [b'sha256', b'sha1'],
599 b'hashes': [b'sha256', b'sha1'],
598 }
600 }
599
601
600 for target in targets:
602 for target in targets:
601 entry = {
603 entry = {
602 b'name': target[b'name'],
604 b'name': target[b'name'],
603 b'protocol': target[b'protocol'],
605 b'protocol': target[b'protocol'],
604 b'uris': target[b'uris'],
606 b'uris': target[b'uris'],
605 }
607 }
606
608
607 for key in (b'snirequired', b'tlsversions'):
609 for key in (b'snirequired', b'tlsversions'):
608 if key in target:
610 if key in target:
609 entry[key] = target[key]
611 entry[key] = target[key]
610
612
613 # pytype: disable=attribute-error
611 caps[b'redirect'][b'targets'].append(entry)
614 caps[b'redirect'][b'targets'].append(entry)
615 # pytype: enable=attribute-error
612
616
613 return proto.addcapabilities(repo, caps)
617 return proto.addcapabilities(repo, caps)
614
618
615
619
616 def getadvertisedredirecttargets(repo, proto):
620 def getadvertisedredirecttargets(repo, proto):
617 """Obtain a list of content redirect targets.
621 """Obtain a list of content redirect targets.
618
622
619 Returns a list containing potential redirect targets that will be
623 Returns a list containing potential redirect targets that will be
620 advertised in capabilities data. Each dict MUST have the following
624 advertised in capabilities data. Each dict MUST have the following
621 keys:
625 keys:
622
626
623 name
627 name
624 The name of this redirect target. This is the identifier clients use
628 The name of this redirect target. This is the identifier clients use
625 to refer to a target. It is transferred as part of every command
629 to refer to a target. It is transferred as part of every command
626 request.
630 request.
627
631
628 protocol
632 protocol
629 Network protocol used by this target. Typically this is the string
633 Network protocol used by this target. Typically this is the string
630 in front of the ``://`` in a URL. e.g. ``https``.
634 in front of the ``://`` in a URL. e.g. ``https``.
631
635
632 uris
636 uris
633 List of representative URIs for this target. Clients can use the
637 List of representative URIs for this target. Clients can use the
634 URIs to test parsing for compatibility or for ordering preference
638 URIs to test parsing for compatibility or for ordering preference
635 for which target to use.
639 for which target to use.
636
640
637 The following optional keys are recognized:
641 The following optional keys are recognized:
638
642
639 snirequired
643 snirequired
640 Bool indicating if Server Name Indication (SNI) is required to
644 Bool indicating if Server Name Indication (SNI) is required to
641 connect to this target.
645 connect to this target.
642
646
643 tlsversions
647 tlsversions
644 List of bytes indicating which TLS versions are supported by this
648 List of bytes indicating which TLS versions are supported by this
645 target.
649 target.
646
650
647 By default, clients reflect the target order advertised by servers
651 By default, clients reflect the target order advertised by servers
648 and servers will use the first client-advertised target when picking
652 and servers will use the first client-advertised target when picking
649 a redirect target. So targets should be advertised in the order the
653 a redirect target. So targets should be advertised in the order the
650 server prefers they be used.
654 server prefers they be used.
651 """
655 """
652 return []
656 return []
653
657
654
658
655 def wireprotocommand(
659 def wireprotocommand(
656 name,
660 name,
657 args=None,
661 args=None,
658 permission=b'push',
662 permission=b'push',
659 cachekeyfn=None,
663 cachekeyfn=None,
660 extracapabilitiesfn=None,
664 extracapabilitiesfn=None,
661 ):
665 ):
662 """Decorator to declare a wire protocol command.
666 """Decorator to declare a wire protocol command.
663
667
664 ``name`` is the name of the wire protocol command being provided.
668 ``name`` is the name of the wire protocol command being provided.
665
669
666 ``args`` is a dict defining arguments accepted by the command. Keys are
670 ``args`` is a dict defining arguments accepted by the command. Keys are
667 the argument name. Values are dicts with the following keys:
671 the argument name. Values are dicts with the following keys:
668
672
669 ``type``
673 ``type``
670 The argument data type. Must be one of the following string
674 The argument data type. Must be one of the following string
671 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
675 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
672 or ``bool``.
676 or ``bool``.
673
677
674 ``default``
678 ``default``
675 A callable returning the default value for this argument. If not
679 A callable returning the default value for this argument. If not
676 specified, ``None`` will be the default value.
680 specified, ``None`` will be the default value.
677
681
678 ``example``
682 ``example``
679 An example value for this argument.
683 An example value for this argument.
680
684
681 ``validvalues``
685 ``validvalues``
682 Set of recognized values for this argument.
686 Set of recognized values for this argument.
683
687
684 ``permission`` defines the permission type needed to run this command.
688 ``permission`` defines the permission type needed to run this command.
685 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
689 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
686 respectively. Default is to assume command requires ``push`` permissions
690 respectively. Default is to assume command requires ``push`` permissions
687 because otherwise commands not declaring their permissions could modify
691 because otherwise commands not declaring their permissions could modify
688 a repository that is supposed to be read-only.
692 a repository that is supposed to be read-only.
689
693
690 ``cachekeyfn`` defines an optional callable that can derive the
694 ``cachekeyfn`` defines an optional callable that can derive the
691 cache key for this request.
695 cache key for this request.
692
696
693 ``extracapabilitiesfn`` defines an optional callable that defines extra
697 ``extracapabilitiesfn`` defines an optional callable that defines extra
694 command capabilities/parameters that are advertised next to the command
698 command capabilities/parameters that are advertised next to the command
695 in the capabilities data structure describing the server. The callable
699 in the capabilities data structure describing the server. The callable
696 receives as arguments the repository and protocol objects. It returns
700 receives as arguments the repository and protocol objects. It returns
697 a dict of extra fields to add to the command descriptor.
701 a dict of extra fields to add to the command descriptor.
698
702
699 Wire protocol commands are generators of objects to be serialized and
703 Wire protocol commands are generators of objects to be serialized and
700 sent to the client.
704 sent to the client.
701
705
702 If a command raises an uncaught exception, this will be translated into
706 If a command raises an uncaught exception, this will be translated into
703 a command error.
707 a command error.
704
708
705 All commands can opt in to being cacheable by defining a function
709 All commands can opt in to being cacheable by defining a function
706 (``cachekeyfn``) that is called to derive a cache key. This function
710 (``cachekeyfn``) that is called to derive a cache key. This function
707 receives the same arguments as the command itself plus a ``cacher``
711 receives the same arguments as the command itself plus a ``cacher``
708 argument containing the active cacher for the request and returns a bytes
712 argument containing the active cacher for the request and returns a bytes
709 containing the key in a cache the response to this command may be cached
713 containing the key in a cache the response to this command may be cached
710 under.
714 under.
711 """
715 """
712 transports = {
716 transports = {
713 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2
717 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2
714 }
718 }
715
719
716 if permission not in (b'push', b'pull'):
720 if permission not in (b'push', b'pull'):
717 raise error.ProgrammingError(
721 raise error.ProgrammingError(
718 b'invalid wire protocol permission; '
722 b'invalid wire protocol permission; '
719 b'got %s; expected "push" or "pull"' % permission
723 b'got %s; expected "push" or "pull"' % permission
720 )
724 )
721
725
722 if args is None:
726 if args is None:
723 args = {}
727 args = {}
724
728
725 if not isinstance(args, dict):
729 if not isinstance(args, dict):
726 raise error.ProgrammingError(
730 raise error.ProgrammingError(
727 b'arguments for version 2 commands must be declared as dicts'
731 b'arguments for version 2 commands must be declared as dicts'
728 )
732 )
729
733
730 for arg, meta in args.items():
734 for arg, meta in args.items():
731 if arg == b'*':
735 if arg == b'*':
732 raise error.ProgrammingError(
736 raise error.ProgrammingError(
733 b'* argument name not allowed on version 2 commands'
737 b'* argument name not allowed on version 2 commands'
734 )
738 )
735
739
736 if not isinstance(meta, dict):
740 if not isinstance(meta, dict):
737 raise error.ProgrammingError(
741 raise error.ProgrammingError(
738 b'arguments for version 2 commands '
742 b'arguments for version 2 commands '
739 b'must declare metadata as a dict'
743 b'must declare metadata as a dict'
740 )
744 )
741
745
742 if b'type' not in meta:
746 if b'type' not in meta:
743 raise error.ProgrammingError(
747 raise error.ProgrammingError(
744 b'%s argument for command %s does not '
748 b'%s argument for command %s does not '
745 b'declare type field' % (arg, name)
749 b'declare type field' % (arg, name)
746 )
750 )
747
751
748 if meta[b'type'] not in (
752 if meta[b'type'] not in (
749 b'bytes',
753 b'bytes',
750 b'int',
754 b'int',
751 b'list',
755 b'list',
752 b'dict',
756 b'dict',
753 b'set',
757 b'set',
754 b'bool',
758 b'bool',
755 ):
759 ):
756 raise error.ProgrammingError(
760 raise error.ProgrammingError(
757 b'%s argument for command %s has '
761 b'%s argument for command %s has '
758 b'illegal type: %s' % (arg, name, meta[b'type'])
762 b'illegal type: %s' % (arg, name, meta[b'type'])
759 )
763 )
760
764
761 if b'example' not in meta:
765 if b'example' not in meta:
762 raise error.ProgrammingError(
766 raise error.ProgrammingError(
763 b'%s argument for command %s does not '
767 b'%s argument for command %s does not '
764 b'declare example field' % (arg, name)
768 b'declare example field' % (arg, name)
765 )
769 )
766
770
767 meta[b'required'] = b'default' not in meta
771 meta[b'required'] = b'default' not in meta
768
772
769 meta.setdefault(b'default', lambda: None)
773 meta.setdefault(b'default', lambda: None)
770 meta.setdefault(b'validvalues', None)
774 meta.setdefault(b'validvalues', None)
771
775
772 def register(func):
776 def register(func):
773 if name in COMMANDS:
777 if name in COMMANDS:
774 raise error.ProgrammingError(
778 raise error.ProgrammingError(
775 b'%s command already registered for version 2' % name
779 b'%s command already registered for version 2' % name
776 )
780 )
777
781
778 COMMANDS[name] = wireprototypes.commandentry(
782 COMMANDS[name] = wireprototypes.commandentry(
779 func,
783 func,
780 args=args,
784 args=args,
781 transports=transports,
785 transports=transports,
782 permission=permission,
786 permission=permission,
783 cachekeyfn=cachekeyfn,
787 cachekeyfn=cachekeyfn,
784 extracapabilitiesfn=extracapabilitiesfn,
788 extracapabilitiesfn=extracapabilitiesfn,
785 )
789 )
786
790
787 return func
791 return func
788
792
789 return register
793 return register
790
794
791
795
792 def makecommandcachekeyfn(command, localversion=None, allargs=False):
796 def makecommandcachekeyfn(command, localversion=None, allargs=False):
793 """Construct a cache key derivation function with common features.
797 """Construct a cache key derivation function with common features.
794
798
795 By default, the cache key is a hash of:
799 By default, the cache key is a hash of:
796
800
797 * The command name.
801 * The command name.
798 * A global cache version number.
802 * A global cache version number.
799 * A local cache version number (passed via ``localversion``).
803 * A local cache version number (passed via ``localversion``).
800 * All the arguments passed to the command.
804 * All the arguments passed to the command.
801 * The media type used.
805 * The media type used.
802 * Wire protocol version string.
806 * Wire protocol version string.
803 * The repository path.
807 * The repository path.
804 """
808 """
805 if not allargs:
809 if not allargs:
806 raise error.ProgrammingError(
810 raise error.ProgrammingError(
807 b'only allargs=True is currently supported'
811 b'only allargs=True is currently supported'
808 )
812 )
809
813
810 if localversion is None:
814 if localversion is None:
811 raise error.ProgrammingError(b'must set localversion argument value')
815 raise error.ProgrammingError(b'must set localversion argument value')
812
816
813 def cachekeyfn(repo, proto, cacher, **args):
817 def cachekeyfn(repo, proto, cacher, **args):
814 spec = COMMANDS[command]
818 spec = COMMANDS[command]
815
819
816 # Commands that mutate the repo can not be cached.
820 # Commands that mutate the repo can not be cached.
817 if spec.permission == b'push':
821 if spec.permission == b'push':
818 return None
822 return None
819
823
820 # TODO config option to disable caching.
824 # TODO config option to disable caching.
821
825
822 # Our key derivation strategy is to construct a data structure
826 # Our key derivation strategy is to construct a data structure
823 # holding everything that could influence cacheability and to hash
827 # holding everything that could influence cacheability and to hash
824 # the CBOR representation of that. Using CBOR seems like it might
828 # the CBOR representation of that. Using CBOR seems like it might
825 # be overkill. However, simpler hashing mechanisms are prone to
829 # be overkill. However, simpler hashing mechanisms are prone to
826 # duplicate input issues. e.g. if you just concatenate two values,
830 # duplicate input issues. e.g. if you just concatenate two values,
827 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
831 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
828 # "padding" between values and prevents these problems.
832 # "padding" between values and prevents these problems.
829
833
830 # Seed the hash with various data.
834 # Seed the hash with various data.
831 state = {
835 state = {
832 # To invalidate all cache keys.
836 # To invalidate all cache keys.
833 b'globalversion': GLOBAL_CACHE_VERSION,
837 b'globalversion': GLOBAL_CACHE_VERSION,
834 # More granular cache key invalidation.
838 # More granular cache key invalidation.
835 b'localversion': localversion,
839 b'localversion': localversion,
836 # Cache keys are segmented by command.
840 # Cache keys are segmented by command.
837 b'command': command,
841 b'command': command,
838 # Throw in the media type and API version strings so changes
842 # Throw in the media type and API version strings so changes
839 # to exchange semantics invalid cache.
843 # to exchange semantics invalid cache.
840 b'mediatype': FRAMINGTYPE,
844 b'mediatype': FRAMINGTYPE,
841 b'version': HTTP_WIREPROTO_V2,
845 b'version': HTTP_WIREPROTO_V2,
842 # So same requests for different repos don't share cache keys.
846 # So same requests for different repos don't share cache keys.
843 b'repo': repo.root,
847 b'repo': repo.root,
844 }
848 }
845
849
846 # The arguments passed to us will have already been normalized.
850 # The arguments passed to us will have already been normalized.
847 # Default values will be set, etc. This is important because it
851 # Default values will be set, etc. This is important because it
848 # means that it doesn't matter if clients send an explicit argument
852 # means that it doesn't matter if clients send an explicit argument
849 # or rely on the default value: it will all normalize to the same
853 # or rely on the default value: it will all normalize to the same
850 # set of arguments on the server and therefore the same cache key.
854 # set of arguments on the server and therefore the same cache key.
851 #
855 #
852 # Arguments by their very nature must support being encoded to CBOR.
856 # Arguments by their very nature must support being encoded to CBOR.
853 # And the CBOR encoder is deterministic. So we hash the arguments
857 # And the CBOR encoder is deterministic. So we hash the arguments
854 # by feeding the CBOR of their representation into the hasher.
858 # by feeding the CBOR of their representation into the hasher.
855 if allargs:
859 if allargs:
856 state[b'args'] = pycompat.byteskwargs(args)
860 state[b'args'] = pycompat.byteskwargs(args)
857
861
858 cacher.adjustcachekeystate(state)
862 cacher.adjustcachekeystate(state)
859
863
860 hasher = hashutil.sha1()
864 hasher = hashutil.sha1()
861 for chunk in cborutil.streamencode(state):
865 for chunk in cborutil.streamencode(state):
862 hasher.update(chunk)
866 hasher.update(chunk)
863
867
864 return pycompat.sysbytes(hasher.hexdigest())
868 return pycompat.sysbytes(hasher.hexdigest())
865
869
866 return cachekeyfn
870 return cachekeyfn
867
871
868
872
869 def makeresponsecacher(
873 def makeresponsecacher(
870 repo, proto, command, args, objencoderfn, redirecttargets, redirecthashes
874 repo, proto, command, args, objencoderfn, redirecttargets, redirecthashes
871 ):
875 ):
872 """Construct a cacher for a cacheable command.
876 """Construct a cacher for a cacheable command.
873
877
874 Returns an ``iwireprotocolcommandcacher`` instance.
878 Returns an ``iwireprotocolcommandcacher`` instance.
875
879
876 Extensions can monkeypatch this function to provide custom caching
880 Extensions can monkeypatch this function to provide custom caching
877 backends.
881 backends.
878 """
882 """
879 return None
883 return None
880
884
881
885
882 def resolvenodes(repo, revisions):
886 def resolvenodes(repo, revisions):
883 """Resolve nodes from a revisions specifier data structure."""
887 """Resolve nodes from a revisions specifier data structure."""
884 cl = repo.changelog
888 cl = repo.changelog
885 clhasnode = cl.hasnode
889 clhasnode = cl.hasnode
886
890
887 seen = set()
891 seen = set()
888 nodes = []
892 nodes = []
889
893
890 if not isinstance(revisions, list):
894 if not isinstance(revisions, list):
891 raise error.WireprotoCommandError(
895 raise error.WireprotoCommandError(
892 b'revisions must be defined as an array'
896 b'revisions must be defined as an array'
893 )
897 )
894
898
895 for spec in revisions:
899 for spec in revisions:
896 if b'type' not in spec:
900 if b'type' not in spec:
897 raise error.WireprotoCommandError(
901 raise error.WireprotoCommandError(
898 b'type key not present in revision specifier'
902 b'type key not present in revision specifier'
899 )
903 )
900
904
901 typ = spec[b'type']
905 typ = spec[b'type']
902
906
903 if typ == b'changesetexplicit':
907 if typ == b'changesetexplicit':
904 if b'nodes' not in spec:
908 if b'nodes' not in spec:
905 raise error.WireprotoCommandError(
909 raise error.WireprotoCommandError(
906 b'nodes key not present in changesetexplicit revision '
910 b'nodes key not present in changesetexplicit revision '
907 b'specifier'
911 b'specifier'
908 )
912 )
909
913
910 for node in spec[b'nodes']:
914 for node in spec[b'nodes']:
911 if node not in seen:
915 if node not in seen:
912 nodes.append(node)
916 nodes.append(node)
913 seen.add(node)
917 seen.add(node)
914
918
915 elif typ == b'changesetexplicitdepth':
919 elif typ == b'changesetexplicitdepth':
916 for key in (b'nodes', b'depth'):
920 for key in (b'nodes', b'depth'):
917 if key not in spec:
921 if key not in spec:
918 raise error.WireprotoCommandError(
922 raise error.WireprotoCommandError(
919 b'%s key not present in changesetexplicitdepth revision '
923 b'%s key not present in changesetexplicitdepth revision '
920 b'specifier',
924 b'specifier',
921 (key,),
925 (key,),
922 )
926 )
923
927
924 for rev in repo.revs(
928 for rev in repo.revs(
925 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
929 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
926 ):
930 ):
927 node = cl.node(rev)
931 node = cl.node(rev)
928
932
929 if node not in seen:
933 if node not in seen:
930 nodes.append(node)
934 nodes.append(node)
931 seen.add(node)
935 seen.add(node)
932
936
933 elif typ == b'changesetdagrange':
937 elif typ == b'changesetdagrange':
934 for key in (b'roots', b'heads'):
938 for key in (b'roots', b'heads'):
935 if key not in spec:
939 if key not in spec:
936 raise error.WireprotoCommandError(
940 raise error.WireprotoCommandError(
937 b'%s key not present in changesetdagrange revision '
941 b'%s key not present in changesetdagrange revision '
938 b'specifier',
942 b'specifier',
939 (key,),
943 (key,),
940 )
944 )
941
945
942 if not spec[b'heads']:
946 if not spec[b'heads']:
943 raise error.WireprotoCommandError(
947 raise error.WireprotoCommandError(
944 b'heads key in changesetdagrange cannot be empty'
948 b'heads key in changesetdagrange cannot be empty'
945 )
949 )
946
950
947 if spec[b'roots']:
951 if spec[b'roots']:
948 common = [n for n in spec[b'roots'] if clhasnode(n)]
952 common = [n for n in spec[b'roots'] if clhasnode(n)]
949 else:
953 else:
950 common = [repo.nullid]
954 common = [repo.nullid]
951
955
952 for n in discovery.outgoing(repo, common, spec[b'heads']).missing:
956 for n in discovery.outgoing(repo, common, spec[b'heads']).missing:
953 if n not in seen:
957 if n not in seen:
954 nodes.append(n)
958 nodes.append(n)
955 seen.add(n)
959 seen.add(n)
956
960
957 else:
961 else:
958 raise error.WireprotoCommandError(
962 raise error.WireprotoCommandError(
959 b'unknown revision specifier type: %s', (typ,)
963 b'unknown revision specifier type: %s', (typ,)
960 )
964 )
961
965
962 return nodes
966 return nodes
963
967
964
968
965 @wireprotocommand(b'branchmap', permission=b'pull')
969 @wireprotocommand(b'branchmap', permission=b'pull')
966 def branchmapv2(repo, proto):
970 def branchmapv2(repo, proto):
967 yield {
971 yield {
968 encoding.fromlocal(k): v
972 encoding.fromlocal(k): v
969 for k, v in pycompat.iteritems(repo.branchmap())
973 for k, v in pycompat.iteritems(repo.branchmap())
970 }
974 }
971
975
972
976
973 @wireprotocommand(b'capabilities', permission=b'pull')
977 @wireprotocommand(b'capabilities', permission=b'pull')
974 def capabilitiesv2(repo, proto):
978 def capabilitiesv2(repo, proto):
975 yield _capabilitiesv2(repo, proto)
979 yield _capabilitiesv2(repo, proto)
976
980
977
981
978 @wireprotocommand(
982 @wireprotocommand(
979 b'changesetdata',
983 b'changesetdata',
980 args={
984 args={
981 b'revisions': {
985 b'revisions': {
982 b'type': b'list',
986 b'type': b'list',
983 b'example': [
987 b'example': [
984 {
988 {
985 b'type': b'changesetexplicit',
989 b'type': b'changesetexplicit',
986 b'nodes': [b'abcdef...'],
990 b'nodes': [b'abcdef...'],
987 }
991 }
988 ],
992 ],
989 },
993 },
990 b'fields': {
994 b'fields': {
991 b'type': b'set',
995 b'type': b'set',
992 b'default': set,
996 b'default': set,
993 b'example': {b'parents', b'revision'},
997 b'example': {b'parents', b'revision'},
994 b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
998 b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
995 },
999 },
996 },
1000 },
997 permission=b'pull',
1001 permission=b'pull',
998 )
1002 )
999 def changesetdata(repo, proto, revisions, fields):
1003 def changesetdata(repo, proto, revisions, fields):
1000 # TODO look for unknown fields and abort when they can't be serviced.
1004 # TODO look for unknown fields and abort when they can't be serviced.
1001 # This could probably be validated by dispatcher using validvalues.
1005 # This could probably be validated by dispatcher using validvalues.
1002
1006
1003 cl = repo.changelog
1007 cl = repo.changelog
1004 outgoing = resolvenodes(repo, revisions)
1008 outgoing = resolvenodes(repo, revisions)
1005 publishing = repo.publishing()
1009 publishing = repo.publishing()
1006
1010
1007 if outgoing:
1011 if outgoing:
1008 repo.hook(b'preoutgoing', throw=True, source=b'serve')
1012 repo.hook(b'preoutgoing', throw=True, source=b'serve')
1009
1013
1010 yield {
1014 yield {
1011 b'totalitems': len(outgoing),
1015 b'totalitems': len(outgoing),
1012 }
1016 }
1013
1017
1014 # The phases of nodes already transferred to the client may have changed
1018 # The phases of nodes already transferred to the client may have changed
1015 # since the client last requested data. We send phase-only records
1019 # since the client last requested data. We send phase-only records
1016 # for these revisions, if requested.
1020 # for these revisions, if requested.
1017 # TODO actually do this. We'll probably want to emit phase heads
1021 # TODO actually do this. We'll probably want to emit phase heads
1018 # in the ancestry set of the outgoing revisions. This will ensure
1022 # in the ancestry set of the outgoing revisions. This will ensure
1019 # that phase updates within that set are seen.
1023 # that phase updates within that set are seen.
1020 if b'phase' in fields:
1024 if b'phase' in fields:
1021 pass
1025 pass
1022
1026
1023 nodebookmarks = {}
1027 nodebookmarks = {}
1024 for mark, node in repo._bookmarks.items():
1028 for mark, node in repo._bookmarks.items():
1025 nodebookmarks.setdefault(node, set()).add(mark)
1029 nodebookmarks.setdefault(node, set()).add(mark)
1026
1030
1027 # It is already topologically sorted by revision number.
1031 # It is already topologically sorted by revision number.
1028 for node in outgoing:
1032 for node in outgoing:
1029 d = {
1033 d = {
1030 b'node': node,
1034 b'node': node,
1031 }
1035 }
1032
1036
1033 if b'parents' in fields:
1037 if b'parents' in fields:
1034 d[b'parents'] = cl.parents(node)
1038 d[b'parents'] = cl.parents(node)
1035
1039
1036 if b'phase' in fields:
1040 if b'phase' in fields:
1037 if publishing:
1041 if publishing:
1038 d[b'phase'] = b'public'
1042 d[b'phase'] = b'public'
1039 else:
1043 else:
1040 ctx = repo[node]
1044 ctx = repo[node]
1041 d[b'phase'] = ctx.phasestr()
1045 d[b'phase'] = ctx.phasestr()
1042
1046
1043 if b'bookmarks' in fields and node in nodebookmarks:
1047 if b'bookmarks' in fields and node in nodebookmarks:
1044 d[b'bookmarks'] = sorted(nodebookmarks[node])
1048 d[b'bookmarks'] = sorted(nodebookmarks[node])
1045 del nodebookmarks[node]
1049 del nodebookmarks[node]
1046
1050
1047 followingmeta = []
1051 followingmeta = []
1048 followingdata = []
1052 followingdata = []
1049
1053
1050 if b'revision' in fields:
1054 if b'revision' in fields:
1051 revisiondata = cl.revision(node)
1055 revisiondata = cl.revision(node)
1052 followingmeta.append((b'revision', len(revisiondata)))
1056 followingmeta.append((b'revision', len(revisiondata)))
1053 followingdata.append(revisiondata)
1057 followingdata.append(revisiondata)
1054
1058
1055 # TODO make it possible for extensions to wrap a function or register
1059 # TODO make it possible for extensions to wrap a function or register
1056 # a handler to service custom fields.
1060 # a handler to service custom fields.
1057
1061
1058 if followingmeta:
1062 if followingmeta:
1059 d[b'fieldsfollowing'] = followingmeta
1063 d[b'fieldsfollowing'] = followingmeta
1060
1064
1061 yield d
1065 yield d
1062
1066
1063 for extra in followingdata:
1067 for extra in followingdata:
1064 yield extra
1068 yield extra
1065
1069
1066 # If requested, send bookmarks from nodes that didn't have revision
1070 # If requested, send bookmarks from nodes that didn't have revision
1067 # data sent so receiver is aware of any bookmark updates.
1071 # data sent so receiver is aware of any bookmark updates.
1068 if b'bookmarks' in fields:
1072 if b'bookmarks' in fields:
1069 for node, marks in sorted(pycompat.iteritems(nodebookmarks)):
1073 for node, marks in sorted(pycompat.iteritems(nodebookmarks)):
1070 yield {
1074 yield {
1071 b'node': node,
1075 b'node': node,
1072 b'bookmarks': sorted(marks),
1076 b'bookmarks': sorted(marks),
1073 }
1077 }
1074
1078
1075
1079
1076 class FileAccessError(Exception):
1080 class FileAccessError(Exception):
1077 """Represents an error accessing a specific file."""
1081 """Represents an error accessing a specific file."""
1078
1082
1079 def __init__(self, path, msg, args):
1083 def __init__(self, path, msg, args):
1080 self.path = path
1084 self.path = path
1081 self.msg = msg
1085 self.msg = msg
1082 self.args = args
1086 self.args = args
1083
1087
1084
1088
1085 def getfilestore(repo, proto, path):
1089 def getfilestore(repo, proto, path):
1086 """Obtain a file storage object for use with wire protocol.
1090 """Obtain a file storage object for use with wire protocol.
1087
1091
1088 Exists as a standalone function so extensions can monkeypatch to add
1092 Exists as a standalone function so extensions can monkeypatch to add
1089 access control.
1093 access control.
1090 """
1094 """
1091 # This seems to work even if the file doesn't exist. So catch
1095 # This seems to work even if the file doesn't exist. So catch
1092 # "empty" files and return an error.
1096 # "empty" files and return an error.
1093 fl = repo.file(path)
1097 fl = repo.file(path)
1094
1098
1095 if not len(fl):
1099 if not len(fl):
1096 raise FileAccessError(path, b'unknown file: %s', (path,))
1100 raise FileAccessError(path, b'unknown file: %s', (path,))
1097
1101
1098 return fl
1102 return fl
1099
1103
1100
1104
1101 def emitfilerevisions(repo, path, revisions, linknodes, fields):
1105 def emitfilerevisions(repo, path, revisions, linknodes, fields):
1102 for revision in revisions:
1106 for revision in revisions:
1103 d = {
1107 d = {
1104 b'node': revision.node,
1108 b'node': revision.node,
1105 }
1109 }
1106
1110
1107 if b'parents' in fields:
1111 if b'parents' in fields:
1108 d[b'parents'] = [revision.p1node, revision.p2node]
1112 d[b'parents'] = [revision.p1node, revision.p2node]
1109
1113
1110 if b'linknode' in fields:
1114 if b'linknode' in fields:
1111 d[b'linknode'] = linknodes[revision.node]
1115 d[b'linknode'] = linknodes[revision.node]
1112
1116
1113 followingmeta = []
1117 followingmeta = []
1114 followingdata = []
1118 followingdata = []
1115
1119
1116 if b'revision' in fields:
1120 if b'revision' in fields:
1117 if revision.revision is not None:
1121 if revision.revision is not None:
1118 followingmeta.append((b'revision', len(revision.revision)))
1122 followingmeta.append((b'revision', len(revision.revision)))
1119 followingdata.append(revision.revision)
1123 followingdata.append(revision.revision)
1120 else:
1124 else:
1121 d[b'deltabasenode'] = revision.basenode
1125 d[b'deltabasenode'] = revision.basenode
1122 followingmeta.append((b'delta', len(revision.delta)))
1126 followingmeta.append((b'delta', len(revision.delta)))
1123 followingdata.append(revision.delta)
1127 followingdata.append(revision.delta)
1124
1128
1125 if followingmeta:
1129 if followingmeta:
1126 d[b'fieldsfollowing'] = followingmeta
1130 d[b'fieldsfollowing'] = followingmeta
1127
1131
1128 yield d
1132 yield d
1129
1133
1130 for extra in followingdata:
1134 for extra in followingdata:
1131 yield extra
1135 yield extra
1132
1136
1133
1137
1134 def makefilematcher(repo, pathfilter):
1138 def makefilematcher(repo, pathfilter):
1135 """Construct a matcher from a path filter dict."""
1139 """Construct a matcher from a path filter dict."""
1136
1140
1137 # Validate values.
1141 # Validate values.
1138 if pathfilter:
1142 if pathfilter:
1139 for key in (b'include', b'exclude'):
1143 for key in (b'include', b'exclude'):
1140 for pattern in pathfilter.get(key, []):
1144 for pattern in pathfilter.get(key, []):
1141 if not pattern.startswith((b'path:', b'rootfilesin:')):
1145 if not pattern.startswith((b'path:', b'rootfilesin:')):
1142 raise error.WireprotoCommandError(
1146 raise error.WireprotoCommandError(
1143 b'%s pattern must begin with `path:` or `rootfilesin:`; '
1147 b'%s pattern must begin with `path:` or `rootfilesin:`; '
1144 b'got %s',
1148 b'got %s',
1145 (key, pattern),
1149 (key, pattern),
1146 )
1150 )
1147
1151
1148 if pathfilter:
1152 if pathfilter:
1149 matcher = matchmod.match(
1153 matcher = matchmod.match(
1150 repo.root,
1154 repo.root,
1151 b'',
1155 b'',
1152 include=pathfilter.get(b'include', []),
1156 include=pathfilter.get(b'include', []),
1153 exclude=pathfilter.get(b'exclude', []),
1157 exclude=pathfilter.get(b'exclude', []),
1154 )
1158 )
1155 else:
1159 else:
1156 matcher = matchmod.match(repo.root, b'')
1160 matcher = matchmod.match(repo.root, b'')
1157
1161
1158 # Requested patterns could include files not in the local store. So
1162 # Requested patterns could include files not in the local store. So
1159 # filter those out.
1163 # filter those out.
1160 return repo.narrowmatch(matcher)
1164 return repo.narrowmatch(matcher)
1161
1165
1162
1166
1163 @wireprotocommand(
1167 @wireprotocommand(
1164 b'filedata',
1168 b'filedata',
1165 args={
1169 args={
1166 b'haveparents': {
1170 b'haveparents': {
1167 b'type': b'bool',
1171 b'type': b'bool',
1168 b'default': lambda: False,
1172 b'default': lambda: False,
1169 b'example': True,
1173 b'example': True,
1170 },
1174 },
1171 b'nodes': {
1175 b'nodes': {
1172 b'type': b'list',
1176 b'type': b'list',
1173 b'example': [b'0123456...'],
1177 b'example': [b'0123456...'],
1174 },
1178 },
1175 b'fields': {
1179 b'fields': {
1176 b'type': b'set',
1180 b'type': b'set',
1177 b'default': set,
1181 b'default': set,
1178 b'example': {b'parents', b'revision'},
1182 b'example': {b'parents', b'revision'},
1179 b'validvalues': {b'parents', b'revision', b'linknode'},
1183 b'validvalues': {b'parents', b'revision', b'linknode'},
1180 },
1184 },
1181 b'path': {
1185 b'path': {
1182 b'type': b'bytes',
1186 b'type': b'bytes',
1183 b'example': b'foo.txt',
1187 b'example': b'foo.txt',
1184 },
1188 },
1185 },
1189 },
1186 permission=b'pull',
1190 permission=b'pull',
1187 # TODO censoring a file revision won't invalidate the cache.
1191 # TODO censoring a file revision won't invalidate the cache.
1188 # Figure out a way to take censoring into account when deriving
1192 # Figure out a way to take censoring into account when deriving
1189 # the cache key.
1193 # the cache key.
1190 cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True),
1194 cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True),
1191 )
1195 )
1192 def filedata(repo, proto, haveparents, nodes, fields, path):
1196 def filedata(repo, proto, haveparents, nodes, fields, path):
1193 # TODO this API allows access to file revisions that are attached to
1197 # TODO this API allows access to file revisions that are attached to
1194 # secret changesets. filesdata does not have this problem. Maybe this
1198 # secret changesets. filesdata does not have this problem. Maybe this
1195 # API should be deleted?
1199 # API should be deleted?
1196
1200
1197 try:
1201 try:
1198 # Extensions may wish to access the protocol handler.
1202 # Extensions may wish to access the protocol handler.
1199 store = getfilestore(repo, proto, path)
1203 store = getfilestore(repo, proto, path)
1200 except FileAccessError as e:
1204 except FileAccessError as e:
1201 raise error.WireprotoCommandError(e.msg, e.args)
1205 raise error.WireprotoCommandError(e.msg, e.args)
1202
1206
1203 clnode = repo.changelog.node
1207 clnode = repo.changelog.node
1204 linknodes = {}
1208 linknodes = {}
1205
1209
1206 # Validate requested nodes.
1210 # Validate requested nodes.
1207 for node in nodes:
1211 for node in nodes:
1208 try:
1212 try:
1209 store.rev(node)
1213 store.rev(node)
1210 except error.LookupError:
1214 except error.LookupError:
1211 raise error.WireprotoCommandError(
1215 raise error.WireprotoCommandError(
1212 b'unknown file node: %s', (hex(node),)
1216 b'unknown file node: %s', (hex(node),)
1213 )
1217 )
1214
1218
1215 # TODO by creating the filectx against a specific file revision
1219 # TODO by creating the filectx against a specific file revision
1216 # instead of changeset, linkrev() is always used. This is wrong for
1220 # instead of changeset, linkrev() is always used. This is wrong for
1217 # cases where linkrev() may refer to a hidden changeset. But since this
1221 # cases where linkrev() may refer to a hidden changeset. But since this
1218 # API doesn't know anything about changesets, we're not sure how to
1222 # API doesn't know anything about changesets, we're not sure how to
1219 # disambiguate the linknode. Perhaps we should delete this API?
1223 # disambiguate the linknode. Perhaps we should delete this API?
1220 fctx = repo.filectx(path, fileid=node)
1224 fctx = repo.filectx(path, fileid=node)
1221 linknodes[node] = clnode(fctx.introrev())
1225 linknodes[node] = clnode(fctx.introrev())
1222
1226
1223 revisions = store.emitrevisions(
1227 revisions = store.emitrevisions(
1224 nodes,
1228 nodes,
1225 revisiondata=b'revision' in fields,
1229 revisiondata=b'revision' in fields,
1226 assumehaveparentrevisions=haveparents,
1230 assumehaveparentrevisions=haveparents,
1227 )
1231 )
1228
1232
1229 yield {
1233 yield {
1230 b'totalitems': len(nodes),
1234 b'totalitems': len(nodes),
1231 }
1235 }
1232
1236
1233 for o in emitfilerevisions(repo, path, revisions, linknodes, fields):
1237 for o in emitfilerevisions(repo, path, revisions, linknodes, fields):
1234 yield o
1238 yield o
1235
1239
1236
1240
1237 def filesdatacapabilities(repo, proto):
1241 def filesdatacapabilities(repo, proto):
1238 batchsize = repo.ui.configint(
1242 batchsize = repo.ui.configint(
1239 b'experimental', b'server.filesdata.recommended-batch-size'
1243 b'experimental', b'server.filesdata.recommended-batch-size'
1240 )
1244 )
1241 return {
1245 return {
1242 b'recommendedbatchsize': batchsize,
1246 b'recommendedbatchsize': batchsize,
1243 }
1247 }
1244
1248
1245
1249
1246 @wireprotocommand(
1250 @wireprotocommand(
1247 b'filesdata',
1251 b'filesdata',
1248 args={
1252 args={
1249 b'haveparents': {
1253 b'haveparents': {
1250 b'type': b'bool',
1254 b'type': b'bool',
1251 b'default': lambda: False,
1255 b'default': lambda: False,
1252 b'example': True,
1256 b'example': True,
1253 },
1257 },
1254 b'fields': {
1258 b'fields': {
1255 b'type': b'set',
1259 b'type': b'set',
1256 b'default': set,
1260 b'default': set,
1257 b'example': {b'parents', b'revision'},
1261 b'example': {b'parents', b'revision'},
1258 b'validvalues': {
1262 b'validvalues': {
1259 b'firstchangeset',
1263 b'firstchangeset',
1260 b'linknode',
1264 b'linknode',
1261 b'parents',
1265 b'parents',
1262 b'revision',
1266 b'revision',
1263 },
1267 },
1264 },
1268 },
1265 b'pathfilter': {
1269 b'pathfilter': {
1266 b'type': b'dict',
1270 b'type': b'dict',
1267 b'default': lambda: None,
1271 b'default': lambda: None,
1268 b'example': {b'include': [b'path:tests']},
1272 b'example': {b'include': [b'path:tests']},
1269 },
1273 },
1270 b'revisions': {
1274 b'revisions': {
1271 b'type': b'list',
1275 b'type': b'list',
1272 b'example': [
1276 b'example': [
1273 {
1277 {
1274 b'type': b'changesetexplicit',
1278 b'type': b'changesetexplicit',
1275 b'nodes': [b'abcdef...'],
1279 b'nodes': [b'abcdef...'],
1276 }
1280 }
1277 ],
1281 ],
1278 },
1282 },
1279 },
1283 },
1280 permission=b'pull',
1284 permission=b'pull',
1281 # TODO censoring a file revision won't invalidate the cache.
1285 # TODO censoring a file revision won't invalidate the cache.
1282 # Figure out a way to take censoring into account when deriving
1286 # Figure out a way to take censoring into account when deriving
1283 # the cache key.
1287 # the cache key.
1284 cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True),
1288 cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True),
1285 extracapabilitiesfn=filesdatacapabilities,
1289 extracapabilitiesfn=filesdatacapabilities,
1286 )
1290 )
1287 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
1291 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
1288 # TODO This should operate on a repo that exposes obsolete changesets. There
1292 # TODO This should operate on a repo that exposes obsolete changesets. There
1289 # is a race between a client making a push that obsoletes a changeset and
1293 # is a race between a client making a push that obsoletes a changeset and
1290 # another client fetching files data for that changeset. If a client has a
1294 # another client fetching files data for that changeset. If a client has a
1291 # changeset, it should probably be allowed to access files data for that
1295 # changeset, it should probably be allowed to access files data for that
1292 # changeset.
1296 # changeset.
1293
1297
1294 outgoing = resolvenodes(repo, revisions)
1298 outgoing = resolvenodes(repo, revisions)
1295 filematcher = makefilematcher(repo, pathfilter)
1299 filematcher = makefilematcher(repo, pathfilter)
1296
1300
1297 # path -> {fnode: linknode}
1301 # path -> {fnode: linknode}
1298 fnodes = collections.defaultdict(dict)
1302 fnodes = collections.defaultdict(dict)
1299
1303
1300 # We collect the set of relevant file revisions by iterating the changeset
1304 # We collect the set of relevant file revisions by iterating the changeset
1301 # revisions and either walking the set of files recorded in the changeset
1305 # revisions and either walking the set of files recorded in the changeset
1302 # or by walking the manifest at that revision. There is probably room for a
1306 # or by walking the manifest at that revision. There is probably room for a
1303 # storage-level API to request this data, as it can be expensive to compute
1307 # storage-level API to request this data, as it can be expensive to compute
1304 # and would benefit from caching or alternate storage from what revlogs
1308 # and would benefit from caching or alternate storage from what revlogs
1305 # provide.
1309 # provide.
1306 for node in outgoing:
1310 for node in outgoing:
1307 ctx = repo[node]
1311 ctx = repo[node]
1308 mctx = ctx.manifestctx()
1312 mctx = ctx.manifestctx()
1309 md = mctx.read()
1313 md = mctx.read()
1310
1314
1311 if haveparents:
1315 if haveparents:
1312 checkpaths = ctx.files()
1316 checkpaths = ctx.files()
1313 else:
1317 else:
1314 checkpaths = md.keys()
1318 checkpaths = md.keys()
1315
1319
1316 for path in checkpaths:
1320 for path in checkpaths:
1317 fnode = md[path]
1321 fnode = md[path]
1318
1322
1319 if path in fnodes and fnode in fnodes[path]:
1323 if path in fnodes and fnode in fnodes[path]:
1320 continue
1324 continue
1321
1325
1322 if not filematcher(path):
1326 if not filematcher(path):
1323 continue
1327 continue
1324
1328
1325 fnodes[path].setdefault(fnode, node)
1329 fnodes[path].setdefault(fnode, node)
1326
1330
1327 yield {
1331 yield {
1328 b'totalpaths': len(fnodes),
1332 b'totalpaths': len(fnodes),
1329 b'totalitems': sum(len(v) for v in fnodes.values()),
1333 b'totalitems': sum(len(v) for v in fnodes.values()),
1330 }
1334 }
1331
1335
1332 for path, filenodes in sorted(fnodes.items()):
1336 for path, filenodes in sorted(fnodes.items()):
1333 try:
1337 try:
1334 store = getfilestore(repo, proto, path)
1338 store = getfilestore(repo, proto, path)
1335 except FileAccessError as e:
1339 except FileAccessError as e:
1336 raise error.WireprotoCommandError(e.msg, e.args)
1340 raise error.WireprotoCommandError(e.msg, e.args)
1337
1341
1338 yield {
1342 yield {
1339 b'path': path,
1343 b'path': path,
1340 b'totalitems': len(filenodes),
1344 b'totalitems': len(filenodes),
1341 }
1345 }
1342
1346
1343 revisions = store.emitrevisions(
1347 revisions = store.emitrevisions(
1344 filenodes.keys(),
1348 filenodes.keys(),
1345 revisiondata=b'revision' in fields,
1349 revisiondata=b'revision' in fields,
1346 assumehaveparentrevisions=haveparents,
1350 assumehaveparentrevisions=haveparents,
1347 )
1351 )
1348
1352
1349 for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
1353 for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
1350 yield o
1354 yield o
1351
1355
1352
1356
1353 @wireprotocommand(
1357 @wireprotocommand(
1354 b'heads',
1358 b'heads',
1355 args={
1359 args={
1356 b'publiconly': {
1360 b'publiconly': {
1357 b'type': b'bool',
1361 b'type': b'bool',
1358 b'default': lambda: False,
1362 b'default': lambda: False,
1359 b'example': False,
1363 b'example': False,
1360 },
1364 },
1361 },
1365 },
1362 permission=b'pull',
1366 permission=b'pull',
1363 )
1367 )
1364 def headsv2(repo, proto, publiconly):
1368 def headsv2(repo, proto, publiconly):
1365 if publiconly:
1369 if publiconly:
1366 repo = repo.filtered(b'immutable')
1370 repo = repo.filtered(b'immutable')
1367
1371
1368 yield repo.heads()
1372 yield repo.heads()
1369
1373
1370
1374
1371 @wireprotocommand(
1375 @wireprotocommand(
1372 b'known',
1376 b'known',
1373 args={
1377 args={
1374 b'nodes': {
1378 b'nodes': {
1375 b'type': b'list',
1379 b'type': b'list',
1376 b'default': list,
1380 b'default': list,
1377 b'example': [b'deadbeef'],
1381 b'example': [b'deadbeef'],
1378 },
1382 },
1379 },
1383 },
1380 permission=b'pull',
1384 permission=b'pull',
1381 )
1385 )
1382 def knownv2(repo, proto, nodes):
1386 def knownv2(repo, proto, nodes):
1383 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1387 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1384 yield result
1388 yield result
1385
1389
1386
1390
1387 @wireprotocommand(
1391 @wireprotocommand(
1388 b'listkeys',
1392 b'listkeys',
1389 args={
1393 args={
1390 b'namespace': {
1394 b'namespace': {
1391 b'type': b'bytes',
1395 b'type': b'bytes',
1392 b'example': b'ns',
1396 b'example': b'ns',
1393 },
1397 },
1394 },
1398 },
1395 permission=b'pull',
1399 permission=b'pull',
1396 )
1400 )
1397 def listkeysv2(repo, proto, namespace):
1401 def listkeysv2(repo, proto, namespace):
1398 keys = repo.listkeys(encoding.tolocal(namespace))
1402 keys = repo.listkeys(encoding.tolocal(namespace))
1399 keys = {
1403 keys = {
1400 encoding.fromlocal(k): encoding.fromlocal(v)
1404 encoding.fromlocal(k): encoding.fromlocal(v)
1401 for k, v in pycompat.iteritems(keys)
1405 for k, v in pycompat.iteritems(keys)
1402 }
1406 }
1403
1407
1404 yield keys
1408 yield keys
1405
1409
1406
1410
1407 @wireprotocommand(
1411 @wireprotocommand(
1408 b'lookup',
1412 b'lookup',
1409 args={
1413 args={
1410 b'key': {
1414 b'key': {
1411 b'type': b'bytes',
1415 b'type': b'bytes',
1412 b'example': b'foo',
1416 b'example': b'foo',
1413 },
1417 },
1414 },
1418 },
1415 permission=b'pull',
1419 permission=b'pull',
1416 )
1420 )
1417 def lookupv2(repo, proto, key):
1421 def lookupv2(repo, proto, key):
1418 key = encoding.tolocal(key)
1422 key = encoding.tolocal(key)
1419
1423
1420 # TODO handle exception.
1424 # TODO handle exception.
1421 node = repo.lookup(key)
1425 node = repo.lookup(key)
1422
1426
1423 yield node
1427 yield node
1424
1428
1425
1429
1426 def manifestdatacapabilities(repo, proto):
1430 def manifestdatacapabilities(repo, proto):
1427 batchsize = repo.ui.configint(
1431 batchsize = repo.ui.configint(
1428 b'experimental', b'server.manifestdata.recommended-batch-size'
1432 b'experimental', b'server.manifestdata.recommended-batch-size'
1429 )
1433 )
1430
1434
1431 return {
1435 return {
1432 b'recommendedbatchsize': batchsize,
1436 b'recommendedbatchsize': batchsize,
1433 }
1437 }
1434
1438
1435
1439
1436 @wireprotocommand(
1440 @wireprotocommand(
1437 b'manifestdata',
1441 b'manifestdata',
1438 args={
1442 args={
1439 b'nodes': {
1443 b'nodes': {
1440 b'type': b'list',
1444 b'type': b'list',
1441 b'example': [b'0123456...'],
1445 b'example': [b'0123456...'],
1442 },
1446 },
1443 b'haveparents': {
1447 b'haveparents': {
1444 b'type': b'bool',
1448 b'type': b'bool',
1445 b'default': lambda: False,
1449 b'default': lambda: False,
1446 b'example': True,
1450 b'example': True,
1447 },
1451 },
1448 b'fields': {
1452 b'fields': {
1449 b'type': b'set',
1453 b'type': b'set',
1450 b'default': set,
1454 b'default': set,
1451 b'example': {b'parents', b'revision'},
1455 b'example': {b'parents', b'revision'},
1452 b'validvalues': {b'parents', b'revision'},
1456 b'validvalues': {b'parents', b'revision'},
1453 },
1457 },
1454 b'tree': {
1458 b'tree': {
1455 b'type': b'bytes',
1459 b'type': b'bytes',
1456 b'example': b'',
1460 b'example': b'',
1457 },
1461 },
1458 },
1462 },
1459 permission=b'pull',
1463 permission=b'pull',
1460 cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True),
1464 cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True),
1461 extracapabilitiesfn=manifestdatacapabilities,
1465 extracapabilitiesfn=manifestdatacapabilities,
1462 )
1466 )
1463 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1467 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1464 store = repo.manifestlog.getstorage(tree)
1468 store = repo.manifestlog.getstorage(tree)
1465
1469
1466 # Validate the node is known and abort on unknown revisions.
1470 # Validate the node is known and abort on unknown revisions.
1467 for node in nodes:
1471 for node in nodes:
1468 try:
1472 try:
1469 store.rev(node)
1473 store.rev(node)
1470 except error.LookupError:
1474 except error.LookupError:
1471 raise error.WireprotoCommandError(b'unknown node: %s', (node,))
1475 raise error.WireprotoCommandError(b'unknown node: %s', (node,))
1472
1476
1473 revisions = store.emitrevisions(
1477 revisions = store.emitrevisions(
1474 nodes,
1478 nodes,
1475 revisiondata=b'revision' in fields,
1479 revisiondata=b'revision' in fields,
1476 assumehaveparentrevisions=haveparents,
1480 assumehaveparentrevisions=haveparents,
1477 )
1481 )
1478
1482
1479 yield {
1483 yield {
1480 b'totalitems': len(nodes),
1484 b'totalitems': len(nodes),
1481 }
1485 }
1482
1486
1483 for revision in revisions:
1487 for revision in revisions:
1484 d = {
1488 d = {
1485 b'node': revision.node,
1489 b'node': revision.node,
1486 }
1490 }
1487
1491
1488 if b'parents' in fields:
1492 if b'parents' in fields:
1489 d[b'parents'] = [revision.p1node, revision.p2node]
1493 d[b'parents'] = [revision.p1node, revision.p2node]
1490
1494
1491 followingmeta = []
1495 followingmeta = []
1492 followingdata = []
1496 followingdata = []
1493
1497
1494 if b'revision' in fields:
1498 if b'revision' in fields:
1495 if revision.revision is not None:
1499 if revision.revision is not None:
1496 followingmeta.append((b'revision', len(revision.revision)))
1500 followingmeta.append((b'revision', len(revision.revision)))
1497 followingdata.append(revision.revision)
1501 followingdata.append(revision.revision)
1498 else:
1502 else:
1499 d[b'deltabasenode'] = revision.basenode
1503 d[b'deltabasenode'] = revision.basenode
1500 followingmeta.append((b'delta', len(revision.delta)))
1504 followingmeta.append((b'delta', len(revision.delta)))
1501 followingdata.append(revision.delta)
1505 followingdata.append(revision.delta)
1502
1506
1503 if followingmeta:
1507 if followingmeta:
1504 d[b'fieldsfollowing'] = followingmeta
1508 d[b'fieldsfollowing'] = followingmeta
1505
1509
1506 yield d
1510 yield d
1507
1511
1508 for extra in followingdata:
1512 for extra in followingdata:
1509 yield extra
1513 yield extra
1510
1514
1511
1515
1512 @wireprotocommand(
1516 @wireprotocommand(
1513 b'pushkey',
1517 b'pushkey',
1514 args={
1518 args={
1515 b'namespace': {
1519 b'namespace': {
1516 b'type': b'bytes',
1520 b'type': b'bytes',
1517 b'example': b'ns',
1521 b'example': b'ns',
1518 },
1522 },
1519 b'key': {
1523 b'key': {
1520 b'type': b'bytes',
1524 b'type': b'bytes',
1521 b'example': b'key',
1525 b'example': b'key',
1522 },
1526 },
1523 b'old': {
1527 b'old': {
1524 b'type': b'bytes',
1528 b'type': b'bytes',
1525 b'example': b'old',
1529 b'example': b'old',
1526 },
1530 },
1527 b'new': {
1531 b'new': {
1528 b'type': b'bytes',
1532 b'type': b'bytes',
1529 b'example': b'new',
1533 b'example': b'new',
1530 },
1534 },
1531 },
1535 },
1532 permission=b'push',
1536 permission=b'push',
1533 )
1537 )
1534 def pushkeyv2(repo, proto, namespace, key, old, new):
1538 def pushkeyv2(repo, proto, namespace, key, old, new):
1535 # TODO handle ui output redirection
1539 # TODO handle ui output redirection
1536 yield repo.pushkey(
1540 yield repo.pushkey(
1537 encoding.tolocal(namespace),
1541 encoding.tolocal(namespace),
1538 encoding.tolocal(key),
1542 encoding.tolocal(key),
1539 encoding.tolocal(old),
1543 encoding.tolocal(old),
1540 encoding.tolocal(new),
1544 encoding.tolocal(new),
1541 )
1545 )
1542
1546
1543
1547
1544 @wireprotocommand(
1548 @wireprotocommand(
1545 b'rawstorefiledata',
1549 b'rawstorefiledata',
1546 args={
1550 args={
1547 b'files': {
1551 b'files': {
1548 b'type': b'list',
1552 b'type': b'list',
1549 b'example': [b'changelog', b'manifestlog'],
1553 b'example': [b'changelog', b'manifestlog'],
1550 },
1554 },
1551 b'pathfilter': {
1555 b'pathfilter': {
1552 b'type': b'list',
1556 b'type': b'list',
1553 b'default': lambda: None,
1557 b'default': lambda: None,
1554 b'example': {b'include': [b'path:tests']},
1558 b'example': {b'include': [b'path:tests']},
1555 },
1559 },
1556 },
1560 },
1557 permission=b'pull',
1561 permission=b'pull',
1558 )
1562 )
1559 def rawstorefiledata(repo, proto, files, pathfilter):
1563 def rawstorefiledata(repo, proto, files, pathfilter):
1560 if not streamclone.allowservergeneration(repo):
1564 if not streamclone.allowservergeneration(repo):
1561 raise error.WireprotoCommandError(b'stream clone is disabled')
1565 raise error.WireprotoCommandError(b'stream clone is disabled')
1562
1566
1563 # TODO support dynamically advertising what store files "sets" are
1567 # TODO support dynamically advertising what store files "sets" are
1564 # available. For now, we support changelog, manifestlog, and files.
1568 # available. For now, we support changelog, manifestlog, and files.
1565 files = set(files)
1569 files = set(files)
1566 allowedfiles = {b'changelog', b'manifestlog'}
1570 allowedfiles = {b'changelog', b'manifestlog'}
1567
1571
1568 unsupported = files - allowedfiles
1572 unsupported = files - allowedfiles
1569 if unsupported:
1573 if unsupported:
1570 raise error.WireprotoCommandError(
1574 raise error.WireprotoCommandError(
1571 b'unknown file type: %s', (b', '.join(sorted(unsupported)),)
1575 b'unknown file type: %s', (b', '.join(sorted(unsupported)),)
1572 )
1576 )
1573
1577
1574 with repo.lock():
1578 with repo.lock():
1575 topfiles = list(repo.store.topfiles())
1579 topfiles = list(repo.store.topfiles())
1576
1580
1577 sendfiles = []
1581 sendfiles = []
1578 totalsize = 0
1582 totalsize = 0
1579
1583
1580 # TODO this is a bunch of storage layer interface abstractions because
1584 # TODO this is a bunch of storage layer interface abstractions because
1581 # it assumes revlogs.
1585 # it assumes revlogs.
1582 for rl_type, name, size in topfiles:
1586 for rl_type, name, size in topfiles:
1583 # XXX use the `rl_type` for that
1587 # XXX use the `rl_type` for that
1584 if b'changelog' in files and name.startswith(b'00changelog'):
1588 if b'changelog' in files and name.startswith(b'00changelog'):
1585 pass
1589 pass
1586 elif b'manifestlog' in files and name.startswith(b'00manifest'):
1590 elif b'manifestlog' in files and name.startswith(b'00manifest'):
1587 pass
1591 pass
1588 else:
1592 else:
1589 continue
1593 continue
1590
1594
1591 sendfiles.append((b'store', name, size))
1595 sendfiles.append((b'store', name, size))
1592 totalsize += size
1596 totalsize += size
1593
1597
1594 yield {
1598 yield {
1595 b'filecount': len(sendfiles),
1599 b'filecount': len(sendfiles),
1596 b'totalsize': totalsize,
1600 b'totalsize': totalsize,
1597 }
1601 }
1598
1602
1599 for location, name, size in sendfiles:
1603 for location, name, size in sendfiles:
1600 yield {
1604 yield {
1601 b'location': location,
1605 b'location': location,
1602 b'path': name,
1606 b'path': name,
1603 b'size': size,
1607 b'size': size,
1604 }
1608 }
1605
1609
1606 # We have to use a closure for this to ensure the context manager is
1610 # We have to use a closure for this to ensure the context manager is
1607 # closed only after sending the final chunk.
1611 # closed only after sending the final chunk.
1608 def getfiledata():
1612 def getfiledata():
1609 with repo.svfs(name, b'rb', auditpath=False) as fh:
1613 with repo.svfs(name, b'rb', auditpath=False) as fh:
1610 for chunk in util.filechunkiter(fh, limit=size):
1614 for chunk in util.filechunkiter(fh, limit=size):
1611 yield chunk
1615 yield chunk
1612
1616
1613 yield wireprototypes.indefinitebytestringresponse(getfiledata())
1617 yield wireprototypes.indefinitebytestringresponse(getfiledata())
@@ -1,92 +1,90 b''
1 #require pytype py3 slow
1 #require pytype py3 slow
2
2
3 $ cd $RUNTESTDIR/..
3 $ cd $RUNTESTDIR/..
4
4
5 Many of the individual files that are excluded here confuse pytype
5 Many of the individual files that are excluded here confuse pytype
6 because they do a mix of Python 2 and Python 3 things
6 because they do a mix of Python 2 and Python 3 things
7 conditionally. There's no good way to help it out with that as far as
7 conditionally. There's no good way to help it out with that as far as
8 I can tell, so let's just hide those files from it for now. We should
8 I can tell, so let's just hide those files from it for now. We should
9 endeavor to empty this list out over time, as some of these are
9 endeavor to empty this list out over time, as some of these are
10 probably hiding real problems.
10 probably hiding real problems.
11
11
12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
13 mercurial/chgserver.py # [attribute-error]
13 mercurial/chgserver.py # [attribute-error]
14 mercurial/context.py # many [attribute-error]
14 mercurial/context.py # many [attribute-error]
15 mercurial/crecord.py # tons of [attribute-error], [module-attr]
15 mercurial/crecord.py # tons of [attribute-error], [module-attr]
16 mercurial/debugcommands.py # [wrong-arg-types]
16 mercurial/debugcommands.py # [wrong-arg-types]
17 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
17 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
18 mercurial/exchange.py # [attribute-error]
18 mercurial/exchange.py # [attribute-error]
19 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
19 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
20 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
20 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
21 mercurial/hgweb/webcommands.py # [missing-parameter]
21 mercurial/hgweb/webcommands.py # [missing-parameter]
22 mercurial/hgweb/wsgicgi.py # confused values in os.environ
22 mercurial/hgweb/wsgicgi.py # confused values in os.environ
23 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
23 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
24 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
24 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
25 mercurial/keepalive.py # [attribute-error]
25 mercurial/keepalive.py # [attribute-error]
26 mercurial/localrepo.py # [attribute-error]
26 mercurial/localrepo.py # [attribute-error]
27 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
27 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
28 mercurial/minirst.py # [unsupported-operands], [attribute-error]
28 mercurial/minirst.py # [unsupported-operands], [attribute-error]
29 mercurial/patch.py # [wrong-arg-types]
29 mercurial/patch.py # [wrong-arg-types]
30 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
30 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
31 mercurial/pure/parsers.py # [attribute-error]
31 mercurial/pure/parsers.py # [attribute-error]
32 mercurial/pycompat.py # bytes vs str issues
32 mercurial/pycompat.py # bytes vs str issues
33 mercurial/repoview.py # [attribute-error]
33 mercurial/repoview.py # [attribute-error]
34 mercurial/sslutil.py # [attribute-error]
34 mercurial/sslutil.py # [attribute-error]
35 mercurial/statprof.py # bytes vs str on TextIO.write() [wrong-arg-types]
35 mercurial/statprof.py # bytes vs str on TextIO.write() [wrong-arg-types]
36 mercurial/testing/storage.py # tons of [attribute-error]
36 mercurial/testing/storage.py # tons of [attribute-error]
37 mercurial/ui.py # [attribute-error], [wrong-arg-types]
37 mercurial/ui.py # [attribute-error], [wrong-arg-types]
38 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
38 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
39 mercurial/util.py # [attribute-error], [wrong-arg-count]
39 mercurial/util.py # [attribute-error], [wrong-arg-count]
40 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
40 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
41 mercurial/utils/memorytop.py # not 3.6 compatible
41 mercurial/utils/memorytop.py # not 3.6 compatible
42 mercurial/win32.py # [not-callable]
42 mercurial/win32.py # [not-callable]
43 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
43 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
44 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
44 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
45 mercurial/wireprotov1peer.py # [attribute-error]
45 mercurial/wireprotov1peer.py # [attribute-error]
46 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
46 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
47 mercurial/wireprotov2server.py # [unsupported-operands], [attribute-error]
48
47
49 TODO: use --no-cache on test server? Caching the files locally helps during
48 TODO: use --no-cache on test server? Caching the files locally helps during
50 development, but may be a hinderance for CI testing.
49 development, but may be a hinderance for CI testing.
51
50
52 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
51 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
53 > -x mercurial/bundlerepo.py \
52 > -x mercurial/bundlerepo.py \
54 > -x mercurial/chgserver.py \
53 > -x mercurial/chgserver.py \
55 > -x mercurial/context.py \
54 > -x mercurial/context.py \
56 > -x mercurial/crecord.py \
55 > -x mercurial/crecord.py \
57 > -x mercurial/debugcommands.py \
56 > -x mercurial/debugcommands.py \
58 > -x mercurial/dispatch.py \
57 > -x mercurial/dispatch.py \
59 > -x mercurial/exchange.py \
58 > -x mercurial/exchange.py \
60 > -x mercurial/hgweb/hgweb_mod.py \
59 > -x mercurial/hgweb/hgweb_mod.py \
61 > -x mercurial/hgweb/server.py \
60 > -x mercurial/hgweb/server.py \
62 > -x mercurial/hgweb/webcommands.py \
61 > -x mercurial/hgweb/webcommands.py \
63 > -x mercurial/hgweb/wsgicgi.py \
62 > -x mercurial/hgweb/wsgicgi.py \
64 > -x mercurial/httppeer.py \
63 > -x mercurial/httppeer.py \
65 > -x mercurial/interfaces \
64 > -x mercurial/interfaces \
66 > -x mercurial/keepalive.py \
65 > -x mercurial/keepalive.py \
67 > -x mercurial/localrepo.py \
66 > -x mercurial/localrepo.py \
68 > -x mercurial/manifest.py \
67 > -x mercurial/manifest.py \
69 > -x mercurial/minirst.py \
68 > -x mercurial/minirst.py \
70 > -x mercurial/patch.py \
69 > -x mercurial/patch.py \
71 > -x mercurial/pure/osutil.py \
70 > -x mercurial/pure/osutil.py \
72 > -x mercurial/pure/parsers.py \
71 > -x mercurial/pure/parsers.py \
73 > -x mercurial/pycompat.py \
72 > -x mercurial/pycompat.py \
74 > -x mercurial/repoview.py \
73 > -x mercurial/repoview.py \
75 > -x mercurial/sslutil.py \
74 > -x mercurial/sslutil.py \
76 > -x mercurial/statprof.py \
75 > -x mercurial/statprof.py \
77 > -x mercurial/testing/storage.py \
76 > -x mercurial/testing/storage.py \
78 > -x mercurial/thirdparty \
77 > -x mercurial/thirdparty \
79 > -x mercurial/ui.py \
78 > -x mercurial/ui.py \
80 > -x mercurial/unionrepo.py \
79 > -x mercurial/unionrepo.py \
81 > -x mercurial/utils/procutil.py \
80 > -x mercurial/utils/procutil.py \
82 > -x mercurial/utils/memorytop.py \
81 > -x mercurial/utils/memorytop.py \
83 > -x mercurial/win32.py \
82 > -x mercurial/win32.py \
84 > -x mercurial/wireprotoframing.py \
83 > -x mercurial/wireprotoframing.py \
85 > -x mercurial/wireprotoserver.py \
84 > -x mercurial/wireprotoserver.py \
86 > -x mercurial/wireprotov1peer.py \
85 > -x mercurial/wireprotov1peer.py \
87 > -x mercurial/wireprotov1server.py \
86 > -x mercurial/wireprotov1server.py \
88 > -x mercurial/wireprotov2server.py \
89 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
87 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
90
88
91 Only show the results on a failure, because the output on success is also
89 Only show the results on a failure, because the output on success is also
92 voluminous and variable.
90 voluminous and variable.
General Comments 0
You need to be logged in to leave comments. Login now