##// END OF EJS Templates
wireprotov2: derive "required" from presence of default value...
Gregory Szorc -
r40031:582676ac default
parent child Browse files
Show More
@@ -1,984 +1,970
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10
10
11 from .i18n import _
11 from .i18n import _
12 from .node import (
12 from .node import (
13 hex,
13 hex,
14 nullid,
14 nullid,
15 )
15 )
16 from . import (
16 from . import (
17 discovery,
17 discovery,
18 encoding,
18 encoding,
19 error,
19 error,
20 narrowspec,
20 narrowspec,
21 pycompat,
21 pycompat,
22 streamclone,
22 streamclone,
23 util,
23 util,
24 wireprotoframing,
24 wireprotoframing,
25 wireprototypes,
25 wireprototypes,
26 )
26 )
27 from .utils import (
27 from .utils import (
28 interfaceutil,
28 interfaceutil,
29 stringutil,
29 stringutil,
30 )
30 )
31
31
32 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
32 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
33
33
34 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
34 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
35
35
36 COMMANDS = wireprototypes.commanddict()
36 COMMANDS = wireprototypes.commanddict()
37
37
38 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
38 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
39 from .hgweb import common as hgwebcommon
39 from .hgweb import common as hgwebcommon
40
40
41 # URL space looks like: <permissions>/<command>, where <permission> can
41 # URL space looks like: <permissions>/<command>, where <permission> can
42 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
42 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
43
43
44 # Root URL does nothing meaningful... yet.
44 # Root URL does nothing meaningful... yet.
45 if not urlparts:
45 if not urlparts:
46 res.status = b'200 OK'
46 res.status = b'200 OK'
47 res.headers[b'Content-Type'] = b'text/plain'
47 res.headers[b'Content-Type'] = b'text/plain'
48 res.setbodybytes(_('HTTP version 2 API handler'))
48 res.setbodybytes(_('HTTP version 2 API handler'))
49 return
49 return
50
50
51 if len(urlparts) == 1:
51 if len(urlparts) == 1:
52 res.status = b'404 Not Found'
52 res.status = b'404 Not Found'
53 res.headers[b'Content-Type'] = b'text/plain'
53 res.headers[b'Content-Type'] = b'text/plain'
54 res.setbodybytes(_('do not know how to process %s\n') %
54 res.setbodybytes(_('do not know how to process %s\n') %
55 req.dispatchpath)
55 req.dispatchpath)
56 return
56 return
57
57
58 permission, command = urlparts[0:2]
58 permission, command = urlparts[0:2]
59
59
60 if permission not in (b'ro', b'rw'):
60 if permission not in (b'ro', b'rw'):
61 res.status = b'404 Not Found'
61 res.status = b'404 Not Found'
62 res.headers[b'Content-Type'] = b'text/plain'
62 res.headers[b'Content-Type'] = b'text/plain'
63 res.setbodybytes(_('unknown permission: %s') % permission)
63 res.setbodybytes(_('unknown permission: %s') % permission)
64 return
64 return
65
65
66 if req.method != 'POST':
66 if req.method != 'POST':
67 res.status = b'405 Method Not Allowed'
67 res.status = b'405 Method Not Allowed'
68 res.headers[b'Allow'] = b'POST'
68 res.headers[b'Allow'] = b'POST'
69 res.setbodybytes(_('commands require POST requests'))
69 res.setbodybytes(_('commands require POST requests'))
70 return
70 return
71
71
72 # At some point we'll want to use our own API instead of recycling the
72 # At some point we'll want to use our own API instead of recycling the
73 # behavior of version 1 of the wire protocol...
73 # behavior of version 1 of the wire protocol...
74 # TODO return reasonable responses - not responses that overload the
74 # TODO return reasonable responses - not responses that overload the
75 # HTTP status line message for error reporting.
75 # HTTP status line message for error reporting.
76 try:
76 try:
77 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
77 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
78 except hgwebcommon.ErrorResponse as e:
78 except hgwebcommon.ErrorResponse as e:
79 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
79 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
80 for k, v in e.headers:
80 for k, v in e.headers:
81 res.headers[k] = v
81 res.headers[k] = v
82 res.setbodybytes('permission denied')
82 res.setbodybytes('permission denied')
83 return
83 return
84
84
85 # We have a special endpoint to reflect the request back at the client.
85 # We have a special endpoint to reflect the request back at the client.
86 if command == b'debugreflect':
86 if command == b'debugreflect':
87 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
87 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
88 return
88 return
89
89
90 # Extra commands that we handle that aren't really wire protocol
90 # Extra commands that we handle that aren't really wire protocol
91 # commands. Think extra hard before making this hackery available to
91 # commands. Think extra hard before making this hackery available to
92 # extension.
92 # extension.
93 extracommands = {'multirequest'}
93 extracommands = {'multirequest'}
94
94
95 if command not in COMMANDS and command not in extracommands:
95 if command not in COMMANDS and command not in extracommands:
96 res.status = b'404 Not Found'
96 res.status = b'404 Not Found'
97 res.headers[b'Content-Type'] = b'text/plain'
97 res.headers[b'Content-Type'] = b'text/plain'
98 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
98 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
99 return
99 return
100
100
101 repo = rctx.repo
101 repo = rctx.repo
102 ui = repo.ui
102 ui = repo.ui
103
103
104 proto = httpv2protocolhandler(req, ui)
104 proto = httpv2protocolhandler(req, ui)
105
105
106 if (not COMMANDS.commandavailable(command, proto)
106 if (not COMMANDS.commandavailable(command, proto)
107 and command not in extracommands):
107 and command not in extracommands):
108 res.status = b'404 Not Found'
108 res.status = b'404 Not Found'
109 res.headers[b'Content-Type'] = b'text/plain'
109 res.headers[b'Content-Type'] = b'text/plain'
110 res.setbodybytes(_('invalid wire protocol command: %s') % command)
110 res.setbodybytes(_('invalid wire protocol command: %s') % command)
111 return
111 return
112
112
113 # TODO consider cases where proxies may add additional Accept headers.
113 # TODO consider cases where proxies may add additional Accept headers.
114 if req.headers.get(b'Accept') != FRAMINGTYPE:
114 if req.headers.get(b'Accept') != FRAMINGTYPE:
115 res.status = b'406 Not Acceptable'
115 res.status = b'406 Not Acceptable'
116 res.headers[b'Content-Type'] = b'text/plain'
116 res.headers[b'Content-Type'] = b'text/plain'
117 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
117 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
118 % FRAMINGTYPE)
118 % FRAMINGTYPE)
119 return
119 return
120
120
121 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
121 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
122 res.status = b'415 Unsupported Media Type'
122 res.status = b'415 Unsupported Media Type'
123 # TODO we should send a response with appropriate media type,
123 # TODO we should send a response with appropriate media type,
124 # since client does Accept it.
124 # since client does Accept it.
125 res.headers[b'Content-Type'] = b'text/plain'
125 res.headers[b'Content-Type'] = b'text/plain'
126 res.setbodybytes(_('client MUST send Content-Type header with '
126 res.setbodybytes(_('client MUST send Content-Type header with '
127 'value: %s\n') % FRAMINGTYPE)
127 'value: %s\n') % FRAMINGTYPE)
128 return
128 return
129
129
130 _processhttpv2request(ui, repo, req, res, permission, command, proto)
130 _processhttpv2request(ui, repo, req, res, permission, command, proto)
131
131
132 def _processhttpv2reflectrequest(ui, repo, req, res):
132 def _processhttpv2reflectrequest(ui, repo, req, res):
133 """Reads unified frame protocol request and dumps out state to client.
133 """Reads unified frame protocol request and dumps out state to client.
134
134
135 This special endpoint can be used to help debug the wire protocol.
135 This special endpoint can be used to help debug the wire protocol.
136
136
137 Instead of routing the request through the normal dispatch mechanism,
137 Instead of routing the request through the normal dispatch mechanism,
138 we instead read all frames, decode them, and feed them into our state
138 we instead read all frames, decode them, and feed them into our state
139 tracker. We then dump the log of all that activity back out to the
139 tracker. We then dump the log of all that activity back out to the
140 client.
140 client.
141 """
141 """
142 import json
142 import json
143
143
144 # Reflection APIs have a history of being abused, accidentally disclosing
144 # Reflection APIs have a history of being abused, accidentally disclosing
145 # sensitive data, etc. So we have a config knob.
145 # sensitive data, etc. So we have a config knob.
146 if not ui.configbool('experimental', 'web.api.debugreflect'):
146 if not ui.configbool('experimental', 'web.api.debugreflect'):
147 res.status = b'404 Not Found'
147 res.status = b'404 Not Found'
148 res.headers[b'Content-Type'] = b'text/plain'
148 res.headers[b'Content-Type'] = b'text/plain'
149 res.setbodybytes(_('debugreflect service not available'))
149 res.setbodybytes(_('debugreflect service not available'))
150 return
150 return
151
151
152 # We assume we have a unified framing protocol request body.
152 # We assume we have a unified framing protocol request body.
153
153
154 reactor = wireprotoframing.serverreactor()
154 reactor = wireprotoframing.serverreactor()
155 states = []
155 states = []
156
156
157 while True:
157 while True:
158 frame = wireprotoframing.readframe(req.bodyfh)
158 frame = wireprotoframing.readframe(req.bodyfh)
159
159
160 if not frame:
160 if not frame:
161 states.append(b'received: <no frame>')
161 states.append(b'received: <no frame>')
162 break
162 break
163
163
164 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
164 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
165 frame.requestid,
165 frame.requestid,
166 frame.payload))
166 frame.payload))
167
167
168 action, meta = reactor.onframerecv(frame)
168 action, meta = reactor.onframerecv(frame)
169 states.append(json.dumps((action, meta), sort_keys=True,
169 states.append(json.dumps((action, meta), sort_keys=True,
170 separators=(', ', ': ')))
170 separators=(', ', ': ')))
171
171
172 action, meta = reactor.oninputeof()
172 action, meta = reactor.oninputeof()
173 meta['action'] = action
173 meta['action'] = action
174 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
174 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
175
175
176 res.status = b'200 OK'
176 res.status = b'200 OK'
177 res.headers[b'Content-Type'] = b'text/plain'
177 res.headers[b'Content-Type'] = b'text/plain'
178 res.setbodybytes(b'\n'.join(states))
178 res.setbodybytes(b'\n'.join(states))
179
179
180 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
180 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
181 """Post-validation handler for HTTPv2 requests.
181 """Post-validation handler for HTTPv2 requests.
182
182
183 Called when the HTTP request contains unified frame-based protocol
183 Called when the HTTP request contains unified frame-based protocol
184 frames for evaluation.
184 frames for evaluation.
185 """
185 """
186 # TODO Some HTTP clients are full duplex and can receive data before
186 # TODO Some HTTP clients are full duplex and can receive data before
187 # the entire request is transmitted. Figure out a way to indicate support
187 # the entire request is transmitted. Figure out a way to indicate support
188 # for that so we can opt into full duplex mode.
188 # for that so we can opt into full duplex mode.
189 reactor = wireprotoframing.serverreactor(deferoutput=True)
189 reactor = wireprotoframing.serverreactor(deferoutput=True)
190 seencommand = False
190 seencommand = False
191
191
192 outstream = reactor.makeoutputstream()
192 outstream = reactor.makeoutputstream()
193
193
194 while True:
194 while True:
195 frame = wireprotoframing.readframe(req.bodyfh)
195 frame = wireprotoframing.readframe(req.bodyfh)
196 if not frame:
196 if not frame:
197 break
197 break
198
198
199 action, meta = reactor.onframerecv(frame)
199 action, meta = reactor.onframerecv(frame)
200
200
201 if action == 'wantframe':
201 if action == 'wantframe':
202 # Need more data before we can do anything.
202 # Need more data before we can do anything.
203 continue
203 continue
204 elif action == 'runcommand':
204 elif action == 'runcommand':
205 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
205 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
206 reqcommand, reactor, outstream,
206 reqcommand, reactor, outstream,
207 meta, issubsequent=seencommand)
207 meta, issubsequent=seencommand)
208
208
209 if sentoutput:
209 if sentoutput:
210 return
210 return
211
211
212 seencommand = True
212 seencommand = True
213
213
214 elif action == 'error':
214 elif action == 'error':
215 # TODO define proper error mechanism.
215 # TODO define proper error mechanism.
216 res.status = b'200 OK'
216 res.status = b'200 OK'
217 res.headers[b'Content-Type'] = b'text/plain'
217 res.headers[b'Content-Type'] = b'text/plain'
218 res.setbodybytes(meta['message'] + b'\n')
218 res.setbodybytes(meta['message'] + b'\n')
219 return
219 return
220 else:
220 else:
221 raise error.ProgrammingError(
221 raise error.ProgrammingError(
222 'unhandled action from frame processor: %s' % action)
222 'unhandled action from frame processor: %s' % action)
223
223
224 action, meta = reactor.oninputeof()
224 action, meta = reactor.oninputeof()
225 if action == 'sendframes':
225 if action == 'sendframes':
226 # We assume we haven't started sending the response yet. If we're
226 # We assume we haven't started sending the response yet. If we're
227 # wrong, the response type will raise an exception.
227 # wrong, the response type will raise an exception.
228 res.status = b'200 OK'
228 res.status = b'200 OK'
229 res.headers[b'Content-Type'] = FRAMINGTYPE
229 res.headers[b'Content-Type'] = FRAMINGTYPE
230 res.setbodygen(meta['framegen'])
230 res.setbodygen(meta['framegen'])
231 elif action == 'noop':
231 elif action == 'noop':
232 pass
232 pass
233 else:
233 else:
234 raise error.ProgrammingError('unhandled action from frame processor: %s'
234 raise error.ProgrammingError('unhandled action from frame processor: %s'
235 % action)
235 % action)
236
236
237 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
237 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
238 outstream, command, issubsequent):
238 outstream, command, issubsequent):
239 """Dispatch a wire protocol command made from HTTPv2 requests.
239 """Dispatch a wire protocol command made from HTTPv2 requests.
240
240
241 The authenticated permission (``authedperm``) along with the original
241 The authenticated permission (``authedperm``) along with the original
242 command from the URL (``reqcommand``) are passed in.
242 command from the URL (``reqcommand``) are passed in.
243 """
243 """
244 # We already validated that the session has permissions to perform the
244 # We already validated that the session has permissions to perform the
245 # actions in ``authedperm``. In the unified frame protocol, the canonical
245 # actions in ``authedperm``. In the unified frame protocol, the canonical
246 # command to run is expressed in a frame. However, the URL also requested
246 # command to run is expressed in a frame. However, the URL also requested
247 # to run a specific command. We need to be careful that the command we
247 # to run a specific command. We need to be careful that the command we
248 # run doesn't have permissions requirements greater than what was granted
248 # run doesn't have permissions requirements greater than what was granted
249 # by ``authedperm``.
249 # by ``authedperm``.
250 #
250 #
251 # Our rule for this is we only allow one command per HTTP request and
251 # Our rule for this is we only allow one command per HTTP request and
252 # that command must match the command in the URL. However, we make
252 # that command must match the command in the URL. However, we make
253 # an exception for the ``multirequest`` URL. This URL is allowed to
253 # an exception for the ``multirequest`` URL. This URL is allowed to
254 # execute multiple commands. We double check permissions of each command
254 # execute multiple commands. We double check permissions of each command
255 # as it is invoked to ensure there is no privilege escalation.
255 # as it is invoked to ensure there is no privilege escalation.
256 # TODO consider allowing multiple commands to regular command URLs
256 # TODO consider allowing multiple commands to regular command URLs
257 # iff each command is the same.
257 # iff each command is the same.
258
258
259 proto = httpv2protocolhandler(req, ui, args=command['args'])
259 proto = httpv2protocolhandler(req, ui, args=command['args'])
260
260
261 if reqcommand == b'multirequest':
261 if reqcommand == b'multirequest':
262 if not COMMANDS.commandavailable(command['command'], proto):
262 if not COMMANDS.commandavailable(command['command'], proto):
263 # TODO proper error mechanism
263 # TODO proper error mechanism
264 res.status = b'200 OK'
264 res.status = b'200 OK'
265 res.headers[b'Content-Type'] = b'text/plain'
265 res.headers[b'Content-Type'] = b'text/plain'
266 res.setbodybytes(_('wire protocol command not available: %s') %
266 res.setbodybytes(_('wire protocol command not available: %s') %
267 command['command'])
267 command['command'])
268 return True
268 return True
269
269
270 # TODO don't use assert here, since it may be elided by -O.
270 # TODO don't use assert here, since it may be elided by -O.
271 assert authedperm in (b'ro', b'rw')
271 assert authedperm in (b'ro', b'rw')
272 wirecommand = COMMANDS[command['command']]
272 wirecommand = COMMANDS[command['command']]
273 assert wirecommand.permission in ('push', 'pull')
273 assert wirecommand.permission in ('push', 'pull')
274
274
275 if authedperm == b'ro' and wirecommand.permission != 'pull':
275 if authedperm == b'ro' and wirecommand.permission != 'pull':
276 # TODO proper error mechanism
276 # TODO proper error mechanism
277 res.status = b'403 Forbidden'
277 res.status = b'403 Forbidden'
278 res.headers[b'Content-Type'] = b'text/plain'
278 res.headers[b'Content-Type'] = b'text/plain'
279 res.setbodybytes(_('insufficient permissions to execute '
279 res.setbodybytes(_('insufficient permissions to execute '
280 'command: %s') % command['command'])
280 'command: %s') % command['command'])
281 return True
281 return True
282
282
283 # TODO should we also call checkperm() here? Maybe not if we're going
283 # TODO should we also call checkperm() here? Maybe not if we're going
284 # to overhaul that API. The granted scope from the URL check should
284 # to overhaul that API. The granted scope from the URL check should
285 # be good enough.
285 # be good enough.
286
286
287 else:
287 else:
288 # Don't allow multiple commands outside of ``multirequest`` URL.
288 # Don't allow multiple commands outside of ``multirequest`` URL.
289 if issubsequent:
289 if issubsequent:
290 # TODO proper error mechanism
290 # TODO proper error mechanism
291 res.status = b'200 OK'
291 res.status = b'200 OK'
292 res.headers[b'Content-Type'] = b'text/plain'
292 res.headers[b'Content-Type'] = b'text/plain'
293 res.setbodybytes(_('multiple commands cannot be issued to this '
293 res.setbodybytes(_('multiple commands cannot be issued to this '
294 'URL'))
294 'URL'))
295 return True
295 return True
296
296
297 if reqcommand != command['command']:
297 if reqcommand != command['command']:
298 # TODO define proper error mechanism
298 # TODO define proper error mechanism
299 res.status = b'200 OK'
299 res.status = b'200 OK'
300 res.headers[b'Content-Type'] = b'text/plain'
300 res.headers[b'Content-Type'] = b'text/plain'
301 res.setbodybytes(_('command in frame must match command in URL'))
301 res.setbodybytes(_('command in frame must match command in URL'))
302 return True
302 return True
303
303
304 res.status = b'200 OK'
304 res.status = b'200 OK'
305 res.headers[b'Content-Type'] = FRAMINGTYPE
305 res.headers[b'Content-Type'] = FRAMINGTYPE
306
306
307 try:
307 try:
308 objs = dispatch(repo, proto, command['command'])
308 objs = dispatch(repo, proto, command['command'])
309
309
310 action, meta = reactor.oncommandresponsereadyobjects(
310 action, meta = reactor.oncommandresponsereadyobjects(
311 outstream, command['requestid'], objs)
311 outstream, command['requestid'], objs)
312
312
313 except error.WireprotoCommandError as e:
313 except error.WireprotoCommandError as e:
314 action, meta = reactor.oncommanderror(
314 action, meta = reactor.oncommanderror(
315 outstream, command['requestid'], e.message, e.messageargs)
315 outstream, command['requestid'], e.message, e.messageargs)
316
316
317 except Exception as e:
317 except Exception as e:
318 action, meta = reactor.onservererror(
318 action, meta = reactor.onservererror(
319 outstream, command['requestid'],
319 outstream, command['requestid'],
320 _('exception when invoking command: %s') %
320 _('exception when invoking command: %s') %
321 stringutil.forcebytestr(e))
321 stringutil.forcebytestr(e))
322
322
323 if action == 'sendframes':
323 if action == 'sendframes':
324 res.setbodygen(meta['framegen'])
324 res.setbodygen(meta['framegen'])
325 return True
325 return True
326 elif action == 'noop':
326 elif action == 'noop':
327 return False
327 return False
328 else:
328 else:
329 raise error.ProgrammingError('unhandled event from reactor: %s' %
329 raise error.ProgrammingError('unhandled event from reactor: %s' %
330 action)
330 action)
331
331
332 def getdispatchrepo(repo, proto, command):
332 def getdispatchrepo(repo, proto, command):
333 return repo.filtered('served')
333 return repo.filtered('served')
334
334
335 def dispatch(repo, proto, command):
335 def dispatch(repo, proto, command):
336 repo = getdispatchrepo(repo, proto, command)
336 repo = getdispatchrepo(repo, proto, command)
337
337
338 func, spec = COMMANDS[command]
338 func, spec = COMMANDS[command]
339 args = proto.getargs(spec)
339 args = proto.getargs(spec)
340
340
341 return func(repo, proto, **pycompat.strkwargs(args))
341 return func(repo, proto, **pycompat.strkwargs(args))
342
342
343 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
343 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
344 class httpv2protocolhandler(object):
344 class httpv2protocolhandler(object):
345 def __init__(self, req, ui, args=None):
345 def __init__(self, req, ui, args=None):
346 self._req = req
346 self._req = req
347 self._ui = ui
347 self._ui = ui
348 self._args = args
348 self._args = args
349
349
350 @property
350 @property
351 def name(self):
351 def name(self):
352 return HTTP_WIREPROTO_V2
352 return HTTP_WIREPROTO_V2
353
353
354 def getargs(self, args):
354 def getargs(self, args):
355 # First look for args that were passed but aren't registered on this
355 # First look for args that were passed but aren't registered on this
356 # command.
356 # command.
357 extra = set(self._args) - set(args)
357 extra = set(self._args) - set(args)
358 if extra:
358 if extra:
359 raise error.WireprotoCommandError(
359 raise error.WireprotoCommandError(
360 'unsupported argument to command: %s' %
360 'unsupported argument to command: %s' %
361 ', '.join(sorted(extra)))
361 ', '.join(sorted(extra)))
362
362
363 # And look for required arguments that are missing.
363 # And look for required arguments that are missing.
364 missing = {a for a in args if args[a]['required']} - set(self._args)
364 missing = {a for a in args if args[a]['required']} - set(self._args)
365
365
366 if missing:
366 if missing:
367 raise error.WireprotoCommandError(
367 raise error.WireprotoCommandError(
368 'missing required arguments: %s' % ', '.join(sorted(missing)))
368 'missing required arguments: %s' % ', '.join(sorted(missing)))
369
369
370 # Now derive the arguments to pass to the command, taking into
370 # Now derive the arguments to pass to the command, taking into
371 # account the arguments specified by the client.
371 # account the arguments specified by the client.
372 data = {}
372 data = {}
373 for k, meta in sorted(args.items()):
373 for k, meta in sorted(args.items()):
374 # This argument wasn't passed by the client.
374 # This argument wasn't passed by the client.
375 if k not in self._args:
375 if k not in self._args:
376 data[k] = meta['default']()
376 data[k] = meta['default']()
377 continue
377 continue
378
378
379 v = self._args[k]
379 v = self._args[k]
380
380
381 # Sets may be expressed as lists. Silently normalize.
381 # Sets may be expressed as lists. Silently normalize.
382 if meta['type'] == 'set' and isinstance(v, list):
382 if meta['type'] == 'set' and isinstance(v, list):
383 v = set(v)
383 v = set(v)
384
384
385 # TODO consider more/stronger type validation.
385 # TODO consider more/stronger type validation.
386
386
387 data[k] = v
387 data[k] = v
388
388
389 return data
389 return data
390
390
391 def getprotocaps(self):
391 def getprotocaps(self):
392 # Protocol capabilities are currently not implemented for HTTP V2.
392 # Protocol capabilities are currently not implemented for HTTP V2.
393 return set()
393 return set()
394
394
395 def getpayload(self):
395 def getpayload(self):
396 raise NotImplementedError
396 raise NotImplementedError
397
397
398 @contextlib.contextmanager
398 @contextlib.contextmanager
399 def mayberedirectstdio(self):
399 def mayberedirectstdio(self):
400 raise NotImplementedError
400 raise NotImplementedError
401
401
402 def client(self):
402 def client(self):
403 raise NotImplementedError
403 raise NotImplementedError
404
404
405 def addcapabilities(self, repo, caps):
405 def addcapabilities(self, repo, caps):
406 return caps
406 return caps
407
407
408 def checkperm(self, perm):
408 def checkperm(self, perm):
409 raise NotImplementedError
409 raise NotImplementedError
410
410
411 def httpv2apidescriptor(req, repo):
411 def httpv2apidescriptor(req, repo):
412 proto = httpv2protocolhandler(req, repo.ui)
412 proto = httpv2protocolhandler(req, repo.ui)
413
413
414 return _capabilitiesv2(repo, proto)
414 return _capabilitiesv2(repo, proto)
415
415
416 def _capabilitiesv2(repo, proto):
416 def _capabilitiesv2(repo, proto):
417 """Obtain the set of capabilities for version 2 transports.
417 """Obtain the set of capabilities for version 2 transports.
418
418
419 These capabilities are distinct from the capabilities for version 1
419 These capabilities are distinct from the capabilities for version 1
420 transports.
420 transports.
421 """
421 """
422 compression = []
422 compression = []
423 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
423 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
424 compression.append({
424 compression.append({
425 b'name': engine.wireprotosupport().name,
425 b'name': engine.wireprotosupport().name,
426 })
426 })
427
427
428 caps = {
428 caps = {
429 'commands': {},
429 'commands': {},
430 'compression': compression,
430 'compression': compression,
431 'framingmediatypes': [FRAMINGTYPE],
431 'framingmediatypes': [FRAMINGTYPE],
432 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
432 'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
433 }
433 }
434
434
435 for command, entry in COMMANDS.items():
435 for command, entry in COMMANDS.items():
436 args = {}
436 args = {}
437
437
438 for arg, meta in entry.args.items():
438 for arg, meta in entry.args.items():
439 args[arg] = {
439 args[arg] = {
440 # TODO should this be a normalized type using CBOR's
440 # TODO should this be a normalized type using CBOR's
441 # terminology?
441 # terminology?
442 b'type': meta['type'],
442 b'type': meta['type'],
443 b'required': meta['required'],
443 b'required': meta['required'],
444 }
444 }
445
445
446 if not meta['required']:
446 if not meta['required']:
447 args[arg][b'default'] = meta['default']()
447 args[arg][b'default'] = meta['default']()
448
448
449 if meta['validvalues']:
449 if meta['validvalues']:
450 args[arg][b'validvalues'] = meta['validvalues']
450 args[arg][b'validvalues'] = meta['validvalues']
451
451
452 caps['commands'][command] = {
452 caps['commands'][command] = {
453 'args': args,
453 'args': args,
454 'permissions': [entry.permission],
454 'permissions': [entry.permission],
455 }
455 }
456
456
457 if streamclone.allowservergeneration(repo):
457 if streamclone.allowservergeneration(repo):
458 caps['rawrepoformats'] = sorted(repo.requirements &
458 caps['rawrepoformats'] = sorted(repo.requirements &
459 repo.supportedformats)
459 repo.supportedformats)
460
460
461 return proto.addcapabilities(repo, caps)
461 return proto.addcapabilities(repo, caps)
462
462
463 def wireprotocommand(name, args=None, permission='push'):
463 def wireprotocommand(name, args=None, permission='push'):
464 """Decorator to declare a wire protocol command.
464 """Decorator to declare a wire protocol command.
465
465
466 ``name`` is the name of the wire protocol command being provided.
466 ``name`` is the name of the wire protocol command being provided.
467
467
468 ``args`` is a dict defining arguments accepted by the command. Keys are
468 ``args`` is a dict defining arguments accepted by the command. Keys are
469 the argument name. Values are dicts with the following keys:
469 the argument name. Values are dicts with the following keys:
470
470
471 ``type``
471 ``type``
472 The argument data type. Must be one of the following string
472 The argument data type. Must be one of the following string
473 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
473 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
474 or ``bool``.
474 or ``bool``.
475
475
476 ``default``
476 ``default``
477 A callable returning the default value for this argument. If not
477 A callable returning the default value for this argument. If not
478 specified, ``None`` will be the default value.
478 specified, ``None`` will be the default value.
479
479
480 ``required``
481 Bool indicating whether the argument is required.
482
483 ``example``
480 ``example``
484 An example value for this argument.
481 An example value for this argument.
485
482
486 ``validvalues``
483 ``validvalues``
487 Set of recognized values for this argument.
484 Set of recognized values for this argument.
488
485
489 ``permission`` defines the permission type needed to run this command.
486 ``permission`` defines the permission type needed to run this command.
490 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
487 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
491 respectively. Default is to assume command requires ``push`` permissions
488 respectively. Default is to assume command requires ``push`` permissions
492 because otherwise commands not declaring their permissions could modify
489 because otherwise commands not declaring their permissions could modify
493 a repository that is supposed to be read-only.
490 a repository that is supposed to be read-only.
494
491
495 Wire protocol commands are generators of objects to be serialized and
492 Wire protocol commands are generators of objects to be serialized and
496 sent to the client.
493 sent to the client.
497
494
498 If a command raises an uncaught exception, this will be translated into
495 If a command raises an uncaught exception, this will be translated into
499 a command error.
496 a command error.
500 """
497 """
501 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
498 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
502 if v['version'] == 2}
499 if v['version'] == 2}
503
500
504 if permission not in ('push', 'pull'):
501 if permission not in ('push', 'pull'):
505 raise error.ProgrammingError('invalid wire protocol permission; '
502 raise error.ProgrammingError('invalid wire protocol permission; '
506 'got %s; expected "push" or "pull"' %
503 'got %s; expected "push" or "pull"' %
507 permission)
504 permission)
508
505
509 if args is None:
506 if args is None:
510 args = {}
507 args = {}
511
508
512 if not isinstance(args, dict):
509 if not isinstance(args, dict):
513 raise error.ProgrammingError('arguments for version 2 commands '
510 raise error.ProgrammingError('arguments for version 2 commands '
514 'must be declared as dicts')
511 'must be declared as dicts')
515
512
516 for arg, meta in args.items():
513 for arg, meta in args.items():
517 if arg == '*':
514 if arg == '*':
518 raise error.ProgrammingError('* argument name not allowed on '
515 raise error.ProgrammingError('* argument name not allowed on '
519 'version 2 commands')
516 'version 2 commands')
520
517
521 if not isinstance(meta, dict):
518 if not isinstance(meta, dict):
522 raise error.ProgrammingError('arguments for version 2 commands '
519 raise error.ProgrammingError('arguments for version 2 commands '
523 'must declare metadata as a dict')
520 'must declare metadata as a dict')
524
521
525 if 'type' not in meta:
522 if 'type' not in meta:
526 raise error.ProgrammingError('%s argument for command %s does not '
523 raise error.ProgrammingError('%s argument for command %s does not '
527 'declare type field' % (arg, name))
524 'declare type field' % (arg, name))
528
525
529 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
526 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
530 raise error.ProgrammingError('%s argument for command %s has '
527 raise error.ProgrammingError('%s argument for command %s has '
531 'illegal type: %s' % (arg, name,
528 'illegal type: %s' % (arg, name,
532 meta['type']))
529 meta['type']))
533
530
534 if 'example' not in meta:
531 if 'example' not in meta:
535 raise error.ProgrammingError('%s argument for command %s does not '
532 raise error.ProgrammingError('%s argument for command %s does not '
536 'declare example field' % (arg, name))
533 'declare example field' % (arg, name))
537
534
538 if 'default' in meta and meta.get('required'):
535 meta['required'] = 'default' not in meta
539 raise error.ProgrammingError('%s argument for command %s is marked '
540 'as required but has a default value' %
541 (arg, name))
542
536
543 meta.setdefault('default', lambda: None)
537 meta.setdefault('default', lambda: None)
544 meta.setdefault('required', False)
545 meta.setdefault('validvalues', None)
538 meta.setdefault('validvalues', None)
546
539
547 def register(func):
540 def register(func):
548 if name in COMMANDS:
541 if name in COMMANDS:
549 raise error.ProgrammingError('%s command already registered '
542 raise error.ProgrammingError('%s command already registered '
550 'for version 2' % name)
543 'for version 2' % name)
551
544
552 COMMANDS[name] = wireprototypes.commandentry(
545 COMMANDS[name] = wireprototypes.commandentry(
553 func, args=args, transports=transports, permission=permission)
546 func, args=args, transports=transports, permission=permission)
554
547
555 return func
548 return func
556
549
557 return register
550 return register
558
551
559 @wireprotocommand('branchmap', permission='pull')
552 @wireprotocommand('branchmap', permission='pull')
560 def branchmapv2(repo, proto):
553 def branchmapv2(repo, proto):
561 yield {encoding.fromlocal(k): v
554 yield {encoding.fromlocal(k): v
562 for k, v in repo.branchmap().iteritems()}
555 for k, v in repo.branchmap().iteritems()}
563
556
564 @wireprotocommand('capabilities', permission='pull')
557 @wireprotocommand('capabilities', permission='pull')
565 def capabilitiesv2(repo, proto):
558 def capabilitiesv2(repo, proto):
566 yield _capabilitiesv2(repo, proto)
559 yield _capabilitiesv2(repo, proto)
567
560
568 @wireprotocommand(
561 @wireprotocommand(
569 'changesetdata',
562 'changesetdata',
570 args={
563 args={
571 'noderange': {
564 'noderange': {
572 'type': 'list',
565 'type': 'list',
566 'default': lambda: None,
573 'example': [[b'0123456...'], [b'abcdef...']],
567 'example': [[b'0123456...'], [b'abcdef...']],
574 },
568 },
575 'nodes': {
569 'nodes': {
576 'type': 'list',
570 'type': 'list',
571 'default': lambda: None,
577 'example': [b'0123456...'],
572 'example': [b'0123456...'],
578 },
573 },
579 'nodesdepth': {
574 'nodesdepth': {
580 'type': 'int',
575 'type': 'int',
576 'default': lambda: None,
581 'example': 10,
577 'example': 10,
582 },
578 },
583 'fields': {
579 'fields': {
584 'type': 'set',
580 'type': 'set',
585 'default': set,
581 'default': set,
586 'example': {b'parents', b'revision'},
582 'example': {b'parents', b'revision'},
587 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
583 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
588 },
584 },
589 },
585 },
590 permission='pull')
586 permission='pull')
591 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
587 def changesetdata(repo, proto, noderange, nodes, nodesdepth, fields):
592 # TODO look for unknown fields and abort when they can't be serviced.
588 # TODO look for unknown fields and abort when they can't be serviced.
593 # This could probably be validated by dispatcher using validvalues.
589 # This could probably be validated by dispatcher using validvalues.
594
590
595 if noderange is None and nodes is None:
591 if noderange is None and nodes is None:
596 raise error.WireprotoCommandError(
592 raise error.WireprotoCommandError(
597 'noderange or nodes must be defined')
593 'noderange or nodes must be defined')
598
594
599 if nodesdepth is not None and nodes is None:
595 if nodesdepth is not None and nodes is None:
600 raise error.WireprotoCommandError(
596 raise error.WireprotoCommandError(
601 'nodesdepth requires the nodes argument')
597 'nodesdepth requires the nodes argument')
602
598
603 if noderange is not None:
599 if noderange is not None:
604 if len(noderange) != 2:
600 if len(noderange) != 2:
605 raise error.WireprotoCommandError(
601 raise error.WireprotoCommandError(
606 'noderange must consist of 2 elements')
602 'noderange must consist of 2 elements')
607
603
608 if not noderange[1]:
604 if not noderange[1]:
609 raise error.WireprotoCommandError(
605 raise error.WireprotoCommandError(
610 'heads in noderange request cannot be empty')
606 'heads in noderange request cannot be empty')
611
607
612 cl = repo.changelog
608 cl = repo.changelog
613 hasnode = cl.hasnode
609 hasnode = cl.hasnode
614
610
615 seen = set()
611 seen = set()
616 outgoing = []
612 outgoing = []
617
613
618 if nodes is not None:
614 if nodes is not None:
619 outgoing = [n for n in nodes if hasnode(n)]
615 outgoing = [n for n in nodes if hasnode(n)]
620
616
621 if nodesdepth:
617 if nodesdepth:
622 outgoing = [cl.node(r) for r in
618 outgoing = [cl.node(r) for r in
623 repo.revs(b'ancestors(%ln, %d)', outgoing,
619 repo.revs(b'ancestors(%ln, %d)', outgoing,
624 nodesdepth - 1)]
620 nodesdepth - 1)]
625
621
626 seen |= set(outgoing)
622 seen |= set(outgoing)
627
623
628 if noderange is not None:
624 if noderange is not None:
629 if noderange[0]:
625 if noderange[0]:
630 common = [n for n in noderange[0] if hasnode(n)]
626 common = [n for n in noderange[0] if hasnode(n)]
631 else:
627 else:
632 common = [nullid]
628 common = [nullid]
633
629
634 for n in discovery.outgoing(repo, common, noderange[1]).missing:
630 for n in discovery.outgoing(repo, common, noderange[1]).missing:
635 if n not in seen:
631 if n not in seen:
636 outgoing.append(n)
632 outgoing.append(n)
637 # Don't need to add to seen here because this is the final
633 # Don't need to add to seen here because this is the final
638 # source of nodes and there should be no duplicates in this
634 # source of nodes and there should be no duplicates in this
639 # list.
635 # list.
640
636
641 seen.clear()
637 seen.clear()
642 publishing = repo.publishing()
638 publishing = repo.publishing()
643
639
644 if outgoing:
640 if outgoing:
645 repo.hook('preoutgoing', throw=True, source='serve')
641 repo.hook('preoutgoing', throw=True, source='serve')
646
642
647 yield {
643 yield {
648 b'totalitems': len(outgoing),
644 b'totalitems': len(outgoing),
649 }
645 }
650
646
651 # The phases of nodes already transferred to the client may have changed
647 # The phases of nodes already transferred to the client may have changed
652 # since the client last requested data. We send phase-only records
648 # since the client last requested data. We send phase-only records
653 # for these revisions, if requested.
649 # for these revisions, if requested.
654 if b'phase' in fields and noderange is not None:
650 if b'phase' in fields and noderange is not None:
655 # TODO skip nodes whose phase will be reflected by a node in the
651 # TODO skip nodes whose phase will be reflected by a node in the
656 # outgoing set. This is purely an optimization to reduce data
652 # outgoing set. This is purely an optimization to reduce data
657 # size.
653 # size.
658 for node in noderange[0]:
654 for node in noderange[0]:
659 yield {
655 yield {
660 b'node': node,
656 b'node': node,
661 b'phase': b'public' if publishing else repo[node].phasestr()
657 b'phase': b'public' if publishing else repo[node].phasestr()
662 }
658 }
663
659
664 nodebookmarks = {}
660 nodebookmarks = {}
665 for mark, node in repo._bookmarks.items():
661 for mark, node in repo._bookmarks.items():
666 nodebookmarks.setdefault(node, set()).add(mark)
662 nodebookmarks.setdefault(node, set()).add(mark)
667
663
668 # It is already topologically sorted by revision number.
664 # It is already topologically sorted by revision number.
669 for node in outgoing:
665 for node in outgoing:
670 d = {
666 d = {
671 b'node': node,
667 b'node': node,
672 }
668 }
673
669
674 if b'parents' in fields:
670 if b'parents' in fields:
675 d[b'parents'] = cl.parents(node)
671 d[b'parents'] = cl.parents(node)
676
672
677 if b'phase' in fields:
673 if b'phase' in fields:
678 if publishing:
674 if publishing:
679 d[b'phase'] = b'public'
675 d[b'phase'] = b'public'
680 else:
676 else:
681 ctx = repo[node]
677 ctx = repo[node]
682 d[b'phase'] = ctx.phasestr()
678 d[b'phase'] = ctx.phasestr()
683
679
684 if b'bookmarks' in fields and node in nodebookmarks:
680 if b'bookmarks' in fields and node in nodebookmarks:
685 d[b'bookmarks'] = sorted(nodebookmarks[node])
681 d[b'bookmarks'] = sorted(nodebookmarks[node])
686 del nodebookmarks[node]
682 del nodebookmarks[node]
687
683
688 followingmeta = []
684 followingmeta = []
689 followingdata = []
685 followingdata = []
690
686
691 if b'revision' in fields:
687 if b'revision' in fields:
692 revisiondata = cl.revision(node, raw=True)
688 revisiondata = cl.revision(node, raw=True)
693 followingmeta.append((b'revision', len(revisiondata)))
689 followingmeta.append((b'revision', len(revisiondata)))
694 followingdata.append(revisiondata)
690 followingdata.append(revisiondata)
695
691
696 # TODO make it possible for extensions to wrap a function or register
692 # TODO make it possible for extensions to wrap a function or register
697 # a handler to service custom fields.
693 # a handler to service custom fields.
698
694
699 if followingmeta:
695 if followingmeta:
700 d[b'fieldsfollowing'] = followingmeta
696 d[b'fieldsfollowing'] = followingmeta
701
697
702 yield d
698 yield d
703
699
704 for extra in followingdata:
700 for extra in followingdata:
705 yield extra
701 yield extra
706
702
707 # If requested, send bookmarks from nodes that didn't have revision
703 # If requested, send bookmarks from nodes that didn't have revision
708 # data sent so receiver is aware of any bookmark updates.
704 # data sent so receiver is aware of any bookmark updates.
709 if b'bookmarks' in fields:
705 if b'bookmarks' in fields:
710 for node, marks in sorted(nodebookmarks.iteritems()):
706 for node, marks in sorted(nodebookmarks.iteritems()):
711 yield {
707 yield {
712 b'node': node,
708 b'node': node,
713 b'bookmarks': sorted(marks),
709 b'bookmarks': sorted(marks),
714 }
710 }
715
711
716 class FileAccessError(Exception):
712 class FileAccessError(Exception):
717 """Represents an error accessing a specific file."""
713 """Represents an error accessing a specific file."""
718
714
719 def __init__(self, path, msg, args):
715 def __init__(self, path, msg, args):
720 self.path = path
716 self.path = path
721 self.msg = msg
717 self.msg = msg
722 self.args = args
718 self.args = args
723
719
724 def getfilestore(repo, proto, path):
720 def getfilestore(repo, proto, path):
725 """Obtain a file storage object for use with wire protocol.
721 """Obtain a file storage object for use with wire protocol.
726
722
727 Exists as a standalone function so extensions can monkeypatch to add
723 Exists as a standalone function so extensions can monkeypatch to add
728 access control.
724 access control.
729 """
725 """
730 # This seems to work even if the file doesn't exist. So catch
726 # This seems to work even if the file doesn't exist. So catch
731 # "empty" files and return an error.
727 # "empty" files and return an error.
732 fl = repo.file(path)
728 fl = repo.file(path)
733
729
734 if not len(fl):
730 if not len(fl):
735 raise FileAccessError(path, 'unknown file: %s', (path,))
731 raise FileAccessError(path, 'unknown file: %s', (path,))
736
732
737 return fl
733 return fl
738
734
739 @wireprotocommand(
735 @wireprotocommand(
740 'filedata',
736 'filedata',
741 args={
737 args={
742 'haveparents': {
738 'haveparents': {
743 'type': 'bool',
739 'type': 'bool',
744 'default': lambda: False,
740 'default': lambda: False,
745 'example': True,
741 'example': True,
746 },
742 },
747 'nodes': {
743 'nodes': {
748 'type': 'list',
744 'type': 'list',
749 'required': True,
750 'example': [b'0123456...'],
745 'example': [b'0123456...'],
751 },
746 },
752 'fields': {
747 'fields': {
753 'type': 'set',
748 'type': 'set',
754 'default': set,
749 'default': set,
755 'example': {b'parents', b'revision'},
750 'example': {b'parents', b'revision'},
756 'validvalues': {b'parents', b'revision'},
751 'validvalues': {b'parents', b'revision'},
757 },
752 },
758 'path': {
753 'path': {
759 'type': 'bytes',
754 'type': 'bytes',
760 'required': True,
761 'example': b'foo.txt',
755 'example': b'foo.txt',
762 }
756 }
763 },
757 },
764 permission='pull')
758 permission='pull')
765 def filedata(repo, proto, haveparents, nodes, fields, path):
759 def filedata(repo, proto, haveparents, nodes, fields, path):
766 try:
760 try:
767 # Extensions may wish to access the protocol handler.
761 # Extensions may wish to access the protocol handler.
768 store = getfilestore(repo, proto, path)
762 store = getfilestore(repo, proto, path)
769 except FileAccessError as e:
763 except FileAccessError as e:
770 raise error.WireprotoCommandError(e.msg, e.args)
764 raise error.WireprotoCommandError(e.msg, e.args)
771
765
772 # Validate requested nodes.
766 # Validate requested nodes.
773 for node in nodes:
767 for node in nodes:
774 try:
768 try:
775 store.rev(node)
769 store.rev(node)
776 except error.LookupError:
770 except error.LookupError:
777 raise error.WireprotoCommandError('unknown file node: %s',
771 raise error.WireprotoCommandError('unknown file node: %s',
778 (hex(node),))
772 (hex(node),))
779
773
780 revisions = store.emitrevisions(nodes,
774 revisions = store.emitrevisions(nodes,
781 revisiondata=b'revision' in fields,
775 revisiondata=b'revision' in fields,
782 assumehaveparentrevisions=haveparents)
776 assumehaveparentrevisions=haveparents)
783
777
784 yield {
778 yield {
785 b'totalitems': len(nodes),
779 b'totalitems': len(nodes),
786 }
780 }
787
781
788 for revision in revisions:
782 for revision in revisions:
789 d = {
783 d = {
790 b'node': revision.node,
784 b'node': revision.node,
791 }
785 }
792
786
793 if b'parents' in fields:
787 if b'parents' in fields:
794 d[b'parents'] = [revision.p1node, revision.p2node]
788 d[b'parents'] = [revision.p1node, revision.p2node]
795
789
796 followingmeta = []
790 followingmeta = []
797 followingdata = []
791 followingdata = []
798
792
799 if b'revision' in fields:
793 if b'revision' in fields:
800 if revision.revision is not None:
794 if revision.revision is not None:
801 followingmeta.append((b'revision', len(revision.revision)))
795 followingmeta.append((b'revision', len(revision.revision)))
802 followingdata.append(revision.revision)
796 followingdata.append(revision.revision)
803 else:
797 else:
804 d[b'deltabasenode'] = revision.basenode
798 d[b'deltabasenode'] = revision.basenode
805 followingmeta.append((b'delta', len(revision.delta)))
799 followingmeta.append((b'delta', len(revision.delta)))
806 followingdata.append(revision.delta)
800 followingdata.append(revision.delta)
807
801
808 if followingmeta:
802 if followingmeta:
809 d[b'fieldsfollowing'] = followingmeta
803 d[b'fieldsfollowing'] = followingmeta
810
804
811 yield d
805 yield d
812
806
813 for extra in followingdata:
807 for extra in followingdata:
814 yield extra
808 yield extra
815
809
816 @wireprotocommand(
810 @wireprotocommand(
817 'heads',
811 'heads',
818 args={
812 args={
819 'publiconly': {
813 'publiconly': {
820 'type': 'bool',
814 'type': 'bool',
821 'default': lambda: False,
815 'default': lambda: False,
822 'example': False,
816 'example': False,
823 },
817 },
824 },
818 },
825 permission='pull')
819 permission='pull')
826 def headsv2(repo, proto, publiconly):
820 def headsv2(repo, proto, publiconly):
827 if publiconly:
821 if publiconly:
828 repo = repo.filtered('immutable')
822 repo = repo.filtered('immutable')
829
823
830 yield repo.heads()
824 yield repo.heads()
831
825
832 @wireprotocommand(
826 @wireprotocommand(
833 'known',
827 'known',
834 args={
828 args={
835 'nodes': {
829 'nodes': {
836 'type': 'list',
830 'type': 'list',
837 'default': list,
831 'default': list,
838 'example': [b'deadbeef'],
832 'example': [b'deadbeef'],
839 },
833 },
840 },
834 },
841 permission='pull')
835 permission='pull')
842 def knownv2(repo, proto, nodes):
836 def knownv2(repo, proto, nodes):
843 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
837 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
844 yield result
838 yield result
845
839
846 @wireprotocommand(
840 @wireprotocommand(
847 'listkeys',
841 'listkeys',
848 args={
842 args={
849 'namespace': {
843 'namespace': {
850 'type': 'bytes',
844 'type': 'bytes',
851 'required': True,
852 'example': b'ns',
845 'example': b'ns',
853 },
846 },
854 },
847 },
855 permission='pull')
848 permission='pull')
856 def listkeysv2(repo, proto, namespace):
849 def listkeysv2(repo, proto, namespace):
857 keys = repo.listkeys(encoding.tolocal(namespace))
850 keys = repo.listkeys(encoding.tolocal(namespace))
858 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
851 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
859 for k, v in keys.iteritems()}
852 for k, v in keys.iteritems()}
860
853
861 yield keys
854 yield keys
862
855
863 @wireprotocommand(
856 @wireprotocommand(
864 'lookup',
857 'lookup',
865 args={
858 args={
866 'key': {
859 'key': {
867 'type': 'bytes',
860 'type': 'bytes',
868 'required': True,
869 'example': b'foo',
861 'example': b'foo',
870 },
862 },
871 },
863 },
872 permission='pull')
864 permission='pull')
873 def lookupv2(repo, proto, key):
865 def lookupv2(repo, proto, key):
874 key = encoding.tolocal(key)
866 key = encoding.tolocal(key)
875
867
876 # TODO handle exception.
868 # TODO handle exception.
877 node = repo.lookup(key)
869 node = repo.lookup(key)
878
870
879 yield node
871 yield node
880
872
881 @wireprotocommand(
873 @wireprotocommand(
882 'manifestdata',
874 'manifestdata',
883 args={
875 args={
884 'nodes': {
876 'nodes': {
885 'type': 'list',
877 'type': 'list',
886 'required': True,
887 'example': [b'0123456...'],
878 'example': [b'0123456...'],
888 },
879 },
889 'haveparents': {
880 'haveparents': {
890 'type': 'bool',
881 'type': 'bool',
891 'default': lambda: False,
882 'default': lambda: False,
892 'example': True,
883 'example': True,
893 },
884 },
894 'fields': {
885 'fields': {
895 'type': 'set',
886 'type': 'set',
896 'default': set,
887 'default': set,
897 'example': {b'parents', b'revision'},
888 'example': {b'parents', b'revision'},
898 'validvalues': {b'parents', b'revision'},
889 'validvalues': {b'parents', b'revision'},
899 },
890 },
900 'tree': {
891 'tree': {
901 'type': 'bytes',
892 'type': 'bytes',
902 'required': True,
903 'example': b'',
893 'example': b'',
904 },
894 },
905 },
895 },
906 permission='pull')
896 permission='pull')
907 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
897 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
908 store = repo.manifestlog.getstorage(tree)
898 store = repo.manifestlog.getstorage(tree)
909
899
910 # Validate the node is known and abort on unknown revisions.
900 # Validate the node is known and abort on unknown revisions.
911 for node in nodes:
901 for node in nodes:
912 try:
902 try:
913 store.rev(node)
903 store.rev(node)
914 except error.LookupError:
904 except error.LookupError:
915 raise error.WireprotoCommandError(
905 raise error.WireprotoCommandError(
916 'unknown node: %s', (node,))
906 'unknown node: %s', (node,))
917
907
918 revisions = store.emitrevisions(nodes,
908 revisions = store.emitrevisions(nodes,
919 revisiondata=b'revision' in fields,
909 revisiondata=b'revision' in fields,
920 assumehaveparentrevisions=haveparents)
910 assumehaveparentrevisions=haveparents)
921
911
922 yield {
912 yield {
923 b'totalitems': len(nodes),
913 b'totalitems': len(nodes),
924 }
914 }
925
915
926 for revision in revisions:
916 for revision in revisions:
927 d = {
917 d = {
928 b'node': revision.node,
918 b'node': revision.node,
929 }
919 }
930
920
931 if b'parents' in fields:
921 if b'parents' in fields:
932 d[b'parents'] = [revision.p1node, revision.p2node]
922 d[b'parents'] = [revision.p1node, revision.p2node]
933
923
934 followingmeta = []
924 followingmeta = []
935 followingdata = []
925 followingdata = []
936
926
937 if b'revision' in fields:
927 if b'revision' in fields:
938 if revision.revision is not None:
928 if revision.revision is not None:
939 followingmeta.append((b'revision', len(revision.revision)))
929 followingmeta.append((b'revision', len(revision.revision)))
940 followingdata.append(revision.revision)
930 followingdata.append(revision.revision)
941 else:
931 else:
942 d[b'deltabasenode'] = revision.basenode
932 d[b'deltabasenode'] = revision.basenode
943 followingmeta.append((b'delta', len(revision.delta)))
933 followingmeta.append((b'delta', len(revision.delta)))
944 followingdata.append(revision.delta)
934 followingdata.append(revision.delta)
945
935
946 if followingmeta:
936 if followingmeta:
947 d[b'fieldsfollowing'] = followingmeta
937 d[b'fieldsfollowing'] = followingmeta
948
938
949 yield d
939 yield d
950
940
951 for extra in followingdata:
941 for extra in followingdata:
952 yield extra
942 yield extra
953
943
954 @wireprotocommand(
944 @wireprotocommand(
955 'pushkey',
945 'pushkey',
956 args={
946 args={
957 'namespace': {
947 'namespace': {
958 'type': 'bytes',
948 'type': 'bytes',
959 'required': True,
960 'example': b'ns',
949 'example': b'ns',
961 },
950 },
962 'key': {
951 'key': {
963 'type': 'bytes',
952 'type': 'bytes',
964 'required': True,
965 'example': b'key',
953 'example': b'key',
966 },
954 },
967 'old': {
955 'old': {
968 'type': 'bytes',
956 'type': 'bytes',
969 'required': True,
970 'example': b'old',
957 'example': b'old',
971 },
958 },
972 'new': {
959 'new': {
973 'type': 'bytes',
960 'type': 'bytes',
974 'required': True,
975 'example': 'new',
961 'example': 'new',
976 },
962 },
977 },
963 },
978 permission='push')
964 permission='push')
979 def pushkeyv2(repo, proto, namespace, key, old, new):
965 def pushkeyv2(repo, proto, namespace, key, old, new):
980 # TODO handle ui output redirection
966 # TODO handle ui output redirection
981 yield repo.pushkey(encoding.tolocal(namespace),
967 yield repo.pushkey(encoding.tolocal(namespace),
982 encoding.tolocal(key),
968 encoding.tolocal(key),
983 encoding.tolocal(old),
969 encoding.tolocal(old),
984 encoding.tolocal(new))
970 encoding.tolocal(new))
General Comments 0
You need to be logged in to leave comments. Login now