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