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