Mercurial > hg
comparison mercurial/wireprotov2server.py @ 43076:2372284d9457
formatting: blacken the codebase
This is using my patch to black
(https://github.com/psf/black/pull/826) so we don't un-wrap collection
literals.
Done with:
hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S
# skip-blame mass-reformatting only
# no-check-commit reformats foo_bar functions
Differential Revision: https://phab.mercurial-scm.org/D6971
author | Augie Fackler <augie@google.com> |
---|---|
date | Sun, 06 Oct 2019 09:45:02 -0400 |
parents | 2c4f656c8e9f |
children | 687b865b95ad |
comparison
equal
deleted
inserted
replaced
43075:57875cf423c9 | 43076:2372284d9457 |
---|---|
26 templatefilters, | 26 templatefilters, |
27 util, | 27 util, |
28 wireprotoframing, | 28 wireprotoframing, |
29 wireprototypes, | 29 wireprototypes, |
30 ) | 30 ) |
31 from .interfaces import ( | 31 from .interfaces import util as interfaceutil |
32 util as interfaceutil, | |
33 ) | |
34 from .utils import ( | 32 from .utils import ( |
35 cborutil, | 33 cborutil, |
36 stringutil, | 34 stringutil, |
37 ) | 35 ) |
38 | 36 |
44 | 42 |
45 # Value inserted into cache key computation function. Change the value to | 43 # Value inserted into cache key computation function. Change the value to |
46 # force new cache keys for every command request. This should be done when | 44 # force new cache keys for every command request. This should be done when |
47 # there is a change to how caching works, etc. | 45 # there is a change to how caching works, etc. |
48 GLOBAL_CACHE_VERSION = 1 | 46 GLOBAL_CACHE_VERSION = 1 |
47 | |
49 | 48 |
50 def handlehttpv2request(rctx, req, res, checkperm, urlparts): | 49 def handlehttpv2request(rctx, req, res, checkperm, urlparts): |
51 from .hgweb import common as hgwebcommon | 50 from .hgweb import common as hgwebcommon |
52 | 51 |
53 # URL space looks like: <permissions>/<command>, where <permission> can | 52 # URL space looks like: <permissions>/<command>, where <permission> can |
61 return | 60 return |
62 | 61 |
63 if len(urlparts) == 1: | 62 if len(urlparts) == 1: |
64 res.status = b'404 Not Found' | 63 res.status = b'404 Not Found' |
65 res.headers[b'Content-Type'] = b'text/plain' | 64 res.headers[b'Content-Type'] = b'text/plain' |
66 res.setbodybytes(_('do not know how to process %s\n') % | 65 res.setbodybytes( |
67 req.dispatchpath) | 66 _('do not know how to process %s\n') % req.dispatchpath |
67 ) | |
68 return | 68 return |
69 | 69 |
70 permission, command = urlparts[0:2] | 70 permission, command = urlparts[0:2] |
71 | 71 |
72 if permission not in (b'ro', b'rw'): | 72 if permission not in (b'ro', b'rw'): |
113 repo = rctx.repo | 113 repo = rctx.repo |
114 ui = repo.ui | 114 ui = repo.ui |
115 | 115 |
116 proto = httpv2protocolhandler(req, ui) | 116 proto = httpv2protocolhandler(req, ui) |
117 | 117 |
118 if (not COMMANDS.commandavailable(command, proto) | 118 if ( |
119 and command not in extracommands): | 119 not COMMANDS.commandavailable(command, proto) |
120 and command not in extracommands | |
121 ): | |
120 res.status = b'404 Not Found' | 122 res.status = b'404 Not Found' |
121 res.headers[b'Content-Type'] = b'text/plain' | 123 res.headers[b'Content-Type'] = b'text/plain' |
122 res.setbodybytes(_('invalid wire protocol command: %s') % command) | 124 res.setbodybytes(_('invalid wire protocol command: %s') % command) |
123 return | 125 return |
124 | 126 |
125 # TODO consider cases where proxies may add additional Accept headers. | 127 # TODO consider cases where proxies may add additional Accept headers. |
126 if req.headers.get(b'Accept') != FRAMINGTYPE: | 128 if req.headers.get(b'Accept') != FRAMINGTYPE: |
127 res.status = b'406 Not Acceptable' | 129 res.status = b'406 Not Acceptable' |
128 res.headers[b'Content-Type'] = b'text/plain' | 130 res.headers[b'Content-Type'] = b'text/plain' |
129 res.setbodybytes(_('client MUST specify Accept header with value: %s\n') | 131 res.setbodybytes( |
130 % FRAMINGTYPE) | 132 _('client MUST specify Accept header with value: %s\n') |
133 % FRAMINGTYPE | |
134 ) | |
131 return | 135 return |
132 | 136 |
133 if req.headers.get(b'Content-Type') != FRAMINGTYPE: | 137 if req.headers.get(b'Content-Type') != FRAMINGTYPE: |
134 res.status = b'415 Unsupported Media Type' | 138 res.status = b'415 Unsupported Media Type' |
135 # TODO we should send a response with appropriate media type, | 139 # TODO we should send a response with appropriate media type, |
136 # since client does Accept it. | 140 # since client does Accept it. |
137 res.headers[b'Content-Type'] = b'text/plain' | 141 res.headers[b'Content-Type'] = b'text/plain' |
138 res.setbodybytes(_('client MUST send Content-Type header with ' | 142 res.setbodybytes( |
139 'value: %s\n') % FRAMINGTYPE) | 143 _('client MUST send Content-Type header with ' 'value: %s\n') |
144 % FRAMINGTYPE | |
145 ) | |
140 return | 146 return |
141 | 147 |
142 _processhttpv2request(ui, repo, req, res, permission, command, proto) | 148 _processhttpv2request(ui, repo, req, res, permission, command, proto) |
149 | |
143 | 150 |
144 def _processhttpv2reflectrequest(ui, repo, req, res): | 151 def _processhttpv2reflectrequest(ui, repo, req, res): |
145 """Reads unified frame protocol request and dumps out state to client. | 152 """Reads unified frame protocol request and dumps out state to client. |
146 | 153 |
147 This special endpoint can be used to help debug the wire protocol. | 154 This special endpoint can be used to help debug the wire protocol. |
169 | 176 |
170 if not frame: | 177 if not frame: |
171 states.append(b'received: <no frame>') | 178 states.append(b'received: <no frame>') |
172 break | 179 break |
173 | 180 |
174 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags, | 181 states.append( |
175 frame.requestid, | 182 b'received: %d %d %d %s' |
176 frame.payload)) | 183 % (frame.typeid, frame.flags, frame.requestid, frame.payload) |
184 ) | |
177 | 185 |
178 action, meta = reactor.onframerecv(frame) | 186 action, meta = reactor.onframerecv(frame) |
179 states.append(templatefilters.json((action, meta))) | 187 states.append(templatefilters.json((action, meta))) |
180 | 188 |
181 action, meta = reactor.oninputeof() | 189 action, meta = reactor.oninputeof() |
183 states.append(templatefilters.json(meta)) | 191 states.append(templatefilters.json(meta)) |
184 | 192 |
185 res.status = b'200 OK' | 193 res.status = b'200 OK' |
186 res.headers[b'Content-Type'] = b'text/plain' | 194 res.headers[b'Content-Type'] = b'text/plain' |
187 res.setbodybytes(b'\n'.join(states)) | 195 res.setbodybytes(b'\n'.join(states)) |
196 | |
188 | 197 |
189 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto): | 198 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto): |
190 """Post-validation handler for HTTPv2 requests. | 199 """Post-validation handler for HTTPv2 requests. |
191 | 200 |
192 Called when the HTTP request contains unified frame-based protocol | 201 Called when the HTTP request contains unified frame-based protocol |
214 # Defer creating output stream because we need to wait for | 223 # Defer creating output stream because we need to wait for |
215 # protocol settings frames so proper encoding can be applied. | 224 # protocol settings frames so proper encoding can be applied. |
216 if not outstream: | 225 if not outstream: |
217 outstream = reactor.makeoutputstream() | 226 outstream = reactor.makeoutputstream() |
218 | 227 |
219 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm, | 228 sentoutput = _httpv2runcommand( |
220 reqcommand, reactor, outstream, | 229 ui, |
221 meta, issubsequent=seencommand) | 230 repo, |
231 req, | |
232 res, | |
233 authedperm, | |
234 reqcommand, | |
235 reactor, | |
236 outstream, | |
237 meta, | |
238 issubsequent=seencommand, | |
239 ) | |
222 | 240 |
223 if sentoutput: | 241 if sentoutput: |
224 return | 242 return |
225 | 243 |
226 seencommand = True | 244 seencommand = True |
231 res.headers[b'Content-Type'] = b'text/plain' | 249 res.headers[b'Content-Type'] = b'text/plain' |
232 res.setbodybytes(meta['message'] + b'\n') | 250 res.setbodybytes(meta['message'] + b'\n') |
233 return | 251 return |
234 else: | 252 else: |
235 raise error.ProgrammingError( | 253 raise error.ProgrammingError( |
236 'unhandled action from frame processor: %s' % action) | 254 'unhandled action from frame processor: %s' % action |
255 ) | |
237 | 256 |
238 action, meta = reactor.oninputeof() | 257 action, meta = reactor.oninputeof() |
239 if action == 'sendframes': | 258 if action == 'sendframes': |
240 # We assume we haven't started sending the response yet. If we're | 259 # We assume we haven't started sending the response yet. If we're |
241 # wrong, the response type will raise an exception. | 260 # wrong, the response type will raise an exception. |
243 res.headers[b'Content-Type'] = FRAMINGTYPE | 262 res.headers[b'Content-Type'] = FRAMINGTYPE |
244 res.setbodygen(meta['framegen']) | 263 res.setbodygen(meta['framegen']) |
245 elif action == 'noop': | 264 elif action == 'noop': |
246 pass | 265 pass |
247 else: | 266 else: |
248 raise error.ProgrammingError('unhandled action from frame processor: %s' | 267 raise error.ProgrammingError( |
249 % action) | 268 'unhandled action from frame processor: %s' % action |
250 | 269 ) |
251 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor, | 270 |
252 outstream, command, issubsequent): | 271 |
272 def _httpv2runcommand( | |
273 ui, | |
274 repo, | |
275 req, | |
276 res, | |
277 authedperm, | |
278 reqcommand, | |
279 reactor, | |
280 outstream, | |
281 command, | |
282 issubsequent, | |
283 ): | |
253 """Dispatch a wire protocol command made from HTTPv2 requests. | 284 """Dispatch a wire protocol command made from HTTPv2 requests. |
254 | 285 |
255 The authenticated permission (``authedperm``) along with the original | 286 The authenticated permission (``authedperm``) along with the original |
256 command from the URL (``reqcommand``) are passed in. | 287 command from the URL (``reqcommand``) are passed in. |
257 """ | 288 """ |
275 if reqcommand == b'multirequest': | 306 if reqcommand == b'multirequest': |
276 if not COMMANDS.commandavailable(command['command'], proto): | 307 if not COMMANDS.commandavailable(command['command'], proto): |
277 # TODO proper error mechanism | 308 # TODO proper error mechanism |
278 res.status = b'200 OK' | 309 res.status = b'200 OK' |
279 res.headers[b'Content-Type'] = b'text/plain' | 310 res.headers[b'Content-Type'] = b'text/plain' |
280 res.setbodybytes(_('wire protocol command not available: %s') % | 311 res.setbodybytes( |
281 command['command']) | 312 _('wire protocol command not available: %s') |
313 % command['command'] | |
314 ) | |
282 return True | 315 return True |
283 | 316 |
284 # TODO don't use assert here, since it may be elided by -O. | 317 # TODO don't use assert here, since it may be elided by -O. |
285 assert authedperm in (b'ro', b'rw') | 318 assert authedperm in (b'ro', b'rw') |
286 wirecommand = COMMANDS[command['command']] | 319 wirecommand = COMMANDS[command['command']] |
288 | 321 |
289 if authedperm == b'ro' and wirecommand.permission != 'pull': | 322 if authedperm == b'ro' and wirecommand.permission != 'pull': |
290 # TODO proper error mechanism | 323 # TODO proper error mechanism |
291 res.status = b'403 Forbidden' | 324 res.status = b'403 Forbidden' |
292 res.headers[b'Content-Type'] = b'text/plain' | 325 res.headers[b'Content-Type'] = b'text/plain' |
293 res.setbodybytes(_('insufficient permissions to execute ' | 326 res.setbodybytes( |
294 'command: %s') % command['command']) | 327 _('insufficient permissions to execute ' 'command: %s') |
328 % command['command'] | |
329 ) | |
295 return True | 330 return True |
296 | 331 |
297 # TODO should we also call checkperm() here? Maybe not if we're going | 332 # TODO should we also call checkperm() here? Maybe not if we're going |
298 # to overhaul that API. The granted scope from the URL check should | 333 # to overhaul that API. The granted scope from the URL check should |
299 # be good enough. | 334 # be good enough. |
302 # Don't allow multiple commands outside of ``multirequest`` URL. | 337 # Don't allow multiple commands outside of ``multirequest`` URL. |
303 if issubsequent: | 338 if issubsequent: |
304 # TODO proper error mechanism | 339 # TODO proper error mechanism |
305 res.status = b'200 OK' | 340 res.status = b'200 OK' |
306 res.headers[b'Content-Type'] = b'text/plain' | 341 res.headers[b'Content-Type'] = b'text/plain' |
307 res.setbodybytes(_('multiple commands cannot be issued to this ' | 342 res.setbodybytes( |
308 'URL')) | 343 _('multiple commands cannot be issued to this ' 'URL') |
344 ) | |
309 return True | 345 return True |
310 | 346 |
311 if reqcommand != command['command']: | 347 if reqcommand != command['command']: |
312 # TODO define proper error mechanism | 348 # TODO define proper error mechanism |
313 res.status = b'200 OK' | 349 res.status = b'200 OK' |
320 | 356 |
321 try: | 357 try: |
322 objs = dispatch(repo, proto, command['command'], command['redirect']) | 358 objs = dispatch(repo, proto, command['command'], command['redirect']) |
323 | 359 |
324 action, meta = reactor.oncommandresponsereadyobjects( | 360 action, meta = reactor.oncommandresponsereadyobjects( |
325 outstream, command['requestid'], objs) | 361 outstream, command['requestid'], objs |
362 ) | |
326 | 363 |
327 except error.WireprotoCommandError as e: | 364 except error.WireprotoCommandError as e: |
328 action, meta = reactor.oncommanderror( | 365 action, meta = reactor.oncommanderror( |
329 outstream, command['requestid'], e.message, e.messageargs) | 366 outstream, command['requestid'], e.message, e.messageargs |
367 ) | |
330 | 368 |
331 except Exception as e: | 369 except Exception as e: |
332 action, meta = reactor.onservererror( | 370 action, meta = reactor.onservererror( |
333 outstream, command['requestid'], | 371 outstream, |
334 _('exception when invoking command: %s') % | 372 command['requestid'], |
335 stringutil.forcebytestr(e)) | 373 _('exception when invoking command: %s') |
374 % stringutil.forcebytestr(e), | |
375 ) | |
336 | 376 |
337 if action == 'sendframes': | 377 if action == 'sendframes': |
338 res.setbodygen(meta['framegen']) | 378 res.setbodygen(meta['framegen']) |
339 return True | 379 return True |
340 elif action == 'noop': | 380 elif action == 'noop': |
341 return False | 381 return False |
342 else: | 382 else: |
343 raise error.ProgrammingError('unhandled event from reactor: %s' % | 383 raise error.ProgrammingError( |
344 action) | 384 'unhandled event from reactor: %s' % action |
385 ) | |
386 | |
345 | 387 |
346 def getdispatchrepo(repo, proto, command): | 388 def getdispatchrepo(repo, proto, command): |
347 viewconfig = repo.ui.config('server', 'view') | 389 viewconfig = repo.ui.config('server', 'view') |
348 return repo.filtered(viewconfig) | 390 return repo.filtered(viewconfig) |
391 | |
349 | 392 |
350 def dispatch(repo, proto, command, redirect): | 393 def dispatch(repo, proto, command, redirect): |
351 """Run a wire protocol command. | 394 """Run a wire protocol command. |
352 | 395 |
353 Returns an iterable of objects that will be sent to the client. | 396 Returns an iterable of objects that will be sent to the client. |
377 redirecthashes = redirect[b'hashes'] | 420 redirecthashes = redirect[b'hashes'] |
378 else: | 421 else: |
379 redirecttargets = [] | 422 redirecttargets = [] |
380 redirecthashes = [] | 423 redirecthashes = [] |
381 | 424 |
382 cacher = makeresponsecacher(repo, proto, command, args, | 425 cacher = makeresponsecacher( |
383 cborutil.streamencode, | 426 repo, |
384 redirecttargets=redirecttargets, | 427 proto, |
385 redirecthashes=redirecthashes) | 428 command, |
429 args, | |
430 cborutil.streamencode, | |
431 redirecttargets=redirecttargets, | |
432 redirecthashes=redirecthashes, | |
433 ) | |
386 | 434 |
387 # But we have no cacher. Do default handling. | 435 # But we have no cacher. Do default handling. |
388 if not cacher: | 436 if not cacher: |
389 for o in callcommand(): | 437 for o in callcommand(): |
390 yield o | 438 yield o |
391 return | 439 return |
392 | 440 |
393 with cacher: | 441 with cacher: |
394 cachekey = entry.cachekeyfn(repo, proto, cacher, | 442 cachekey = entry.cachekeyfn( |
395 **pycompat.strkwargs(args)) | 443 repo, proto, cacher, **pycompat.strkwargs(args) |
444 ) | |
396 | 445 |
397 # No cache key or the cacher doesn't like it. Do default handling. | 446 # No cache key or the cacher doesn't like it. Do default handling. |
398 if cachekey is None or not cacher.setcachekey(cachekey): | 447 if cachekey is None or not cacher.setcachekey(cachekey): |
399 for o in callcommand(): | 448 for o in callcommand(): |
400 yield o | 449 yield o |
414 for o in cacher.onobject(o): | 463 for o in cacher.onobject(o): |
415 yield o | 464 yield o |
416 | 465 |
417 for o in cacher.onfinished(): | 466 for o in cacher.onfinished(): |
418 yield o | 467 yield o |
468 | |
419 | 469 |
420 @interfaceutil.implementer(wireprototypes.baseprotocolhandler) | 470 @interfaceutil.implementer(wireprototypes.baseprotocolhandler) |
421 class httpv2protocolhandler(object): | 471 class httpv2protocolhandler(object): |
422 def __init__(self, req, ui, args=None): | 472 def __init__(self, req, ui, args=None): |
423 self._req = req | 473 self._req = req |
432 # First look for args that were passed but aren't registered on this | 482 # First look for args that were passed but aren't registered on this |
433 # command. | 483 # command. |
434 extra = set(self._args) - set(args) | 484 extra = set(self._args) - set(args) |
435 if extra: | 485 if extra: |
436 raise error.WireprotoCommandError( | 486 raise error.WireprotoCommandError( |
437 'unsupported argument to command: %s' % | 487 'unsupported argument to command: %s' % ', '.join(sorted(extra)) |
438 ', '.join(sorted(extra))) | 488 ) |
439 | 489 |
440 # And look for required arguments that are missing. | 490 # And look for required arguments that are missing. |
441 missing = {a for a in args if args[a]['required']} - set(self._args) | 491 missing = {a for a in args if args[a]['required']} - set(self._args) |
442 | 492 |
443 if missing: | 493 if missing: |
444 raise error.WireprotoCommandError( | 494 raise error.WireprotoCommandError( |
445 'missing required arguments: %s' % ', '.join(sorted(missing))) | 495 'missing required arguments: %s' % ', '.join(sorted(missing)) |
496 ) | |
446 | 497 |
447 # Now derive the arguments to pass to the command, taking into | 498 # Now derive the arguments to pass to the command, taking into |
448 # account the arguments specified by the client. | 499 # account the arguments specified by the client. |
449 data = {} | 500 data = {} |
450 for k, meta in sorted(args.items()): | 501 for k, meta in sorted(args.items()): |
483 return caps | 534 return caps |
484 | 535 |
485 def checkperm(self, perm): | 536 def checkperm(self, perm): |
486 raise NotImplementedError | 537 raise NotImplementedError |
487 | 538 |
539 | |
488 def httpv2apidescriptor(req, repo): | 540 def httpv2apidescriptor(req, repo): |
489 proto = httpv2protocolhandler(req, repo.ui) | 541 proto = httpv2protocolhandler(req, repo.ui) |
490 | 542 |
491 return _capabilitiesv2(repo, proto) | 543 return _capabilitiesv2(repo, proto) |
544 | |
492 | 545 |
493 def _capabilitiesv2(repo, proto): | 546 def _capabilitiesv2(repo, proto): |
494 """Obtain the set of capabilities for version 2 transports. | 547 """Obtain the set of capabilities for version 2 transports. |
495 | 548 |
496 These capabilities are distinct from the capabilities for version 1 | 549 These capabilities are distinct from the capabilities for version 1 |
518 | 571 |
519 if meta['validvalues']: | 572 if meta['validvalues']: |
520 args[arg][b'validvalues'] = meta['validvalues'] | 573 args[arg][b'validvalues'] = meta['validvalues'] |
521 | 574 |
522 # TODO this type of check should be defined in a per-command callback. | 575 # TODO this type of check should be defined in a per-command callback. |
523 if (command == b'rawstorefiledata' | 576 if ( |
524 and not streamclone.allowservergeneration(repo)): | 577 command == b'rawstorefiledata' |
578 and not streamclone.allowservergeneration(repo) | |
579 ): | |
525 continue | 580 continue |
526 | 581 |
527 caps['commands'][command] = { | 582 caps['commands'][command] = { |
528 'args': args, | 583 'args': args, |
529 'permissions': [entry.permission], | 584 'permissions': [entry.permission], |
531 | 586 |
532 if entry.extracapabilitiesfn: | 587 if entry.extracapabilitiesfn: |
533 extracaps = entry.extracapabilitiesfn(repo, proto) | 588 extracaps = entry.extracapabilitiesfn(repo, proto) |
534 caps['commands'][command].update(extracaps) | 589 caps['commands'][command].update(extracaps) |
535 | 590 |
536 caps['rawrepoformats'] = sorted(repo.requirements & | 591 caps['rawrepoformats'] = sorted(repo.requirements & repo.supportedformats) |
537 repo.supportedformats) | |
538 | 592 |
539 targets = getadvertisedredirecttargets(repo, proto) | 593 targets = getadvertisedredirecttargets(repo, proto) |
540 if targets: | 594 if targets: |
541 caps[b'redirect'] = { | 595 caps[b'redirect'] = { |
542 b'targets': [], | 596 b'targets': [], |
556 | 610 |
557 caps[b'redirect'][b'targets'].append(entry) | 611 caps[b'redirect'][b'targets'].append(entry) |
558 | 612 |
559 return proto.addcapabilities(repo, caps) | 613 return proto.addcapabilities(repo, caps) |
560 | 614 |
615 | |
561 def getadvertisedredirecttargets(repo, proto): | 616 def getadvertisedredirecttargets(repo, proto): |
562 """Obtain a list of content redirect targets. | 617 """Obtain a list of content redirect targets. |
563 | 618 |
564 Returns a list containing potential redirect targets that will be | 619 Returns a list containing potential redirect targets that will be |
565 advertised in capabilities data. Each dict MUST have the following | 620 advertised in capabilities data. Each dict MUST have the following |
594 a redirect target. So targets should be advertised in the order the | 649 a redirect target. So targets should be advertised in the order the |
595 server prefers they be used. | 650 server prefers they be used. |
596 """ | 651 """ |
597 return [] | 652 return [] |
598 | 653 |
599 def wireprotocommand(name, args=None, permission='push', cachekeyfn=None, | 654 |
600 extracapabilitiesfn=None): | 655 def wireprotocommand( |
656 name, | |
657 args=None, | |
658 permission='push', | |
659 cachekeyfn=None, | |
660 extracapabilitiesfn=None, | |
661 ): | |
601 """Decorator to declare a wire protocol command. | 662 """Decorator to declare a wire protocol command. |
602 | 663 |
603 ``name`` is the name of the wire protocol command being provided. | 664 ``name`` is the name of the wire protocol command being provided. |
604 | 665 |
605 ``args`` is a dict defining arguments accepted by the command. Keys are | 666 ``args`` is a dict defining arguments accepted by the command. Keys are |
646 receives the same arguments as the command itself plus a ``cacher`` | 707 receives the same arguments as the command itself plus a ``cacher`` |
647 argument containing the active cacher for the request and returns a bytes | 708 argument containing the active cacher for the request and returns a bytes |
648 containing the key in a cache the response to this command may be cached | 709 containing the key in a cache the response to this command may be cached |
649 under. | 710 under. |
650 """ | 711 """ |
651 transports = {k for k, v in wireprototypes.TRANSPORTS.items() | 712 transports = { |
652 if v['version'] == 2} | 713 k for k, v in wireprototypes.TRANSPORTS.items() if v['version'] == 2 |
714 } | |
653 | 715 |
654 if permission not in ('push', 'pull'): | 716 if permission not in ('push', 'pull'): |
655 raise error.ProgrammingError('invalid wire protocol permission; ' | 717 raise error.ProgrammingError( |
656 'got %s; expected "push" or "pull"' % | 718 'invalid wire protocol permission; ' |
657 permission) | 719 'got %s; expected "push" or "pull"' % permission |
720 ) | |
658 | 721 |
659 if args is None: | 722 if args is None: |
660 args = {} | 723 args = {} |
661 | 724 |
662 if not isinstance(args, dict): | 725 if not isinstance(args, dict): |
663 raise error.ProgrammingError('arguments for version 2 commands ' | 726 raise error.ProgrammingError( |
664 'must be declared as dicts') | 727 'arguments for version 2 commands ' 'must be declared as dicts' |
728 ) | |
665 | 729 |
666 for arg, meta in args.items(): | 730 for arg, meta in args.items(): |
667 if arg == '*': | 731 if arg == '*': |
668 raise error.ProgrammingError('* argument name not allowed on ' | 732 raise error.ProgrammingError( |
669 'version 2 commands') | 733 '* argument name not allowed on ' 'version 2 commands' |
734 ) | |
670 | 735 |
671 if not isinstance(meta, dict): | 736 if not isinstance(meta, dict): |
672 raise error.ProgrammingError('arguments for version 2 commands ' | 737 raise error.ProgrammingError( |
673 'must declare metadata as a dict') | 738 'arguments for version 2 commands ' |
739 'must declare metadata as a dict' | |
740 ) | |
674 | 741 |
675 if 'type' not in meta: | 742 if 'type' not in meta: |
676 raise error.ProgrammingError('%s argument for command %s does not ' | 743 raise error.ProgrammingError( |
677 'declare type field' % (arg, name)) | 744 '%s argument for command %s does not ' |
745 'declare type field' % (arg, name) | |
746 ) | |
678 | 747 |
679 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'): | 748 if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'): |
680 raise error.ProgrammingError('%s argument for command %s has ' | 749 raise error.ProgrammingError( |
681 'illegal type: %s' % (arg, name, | 750 '%s argument for command %s has ' |
682 meta['type'])) | 751 'illegal type: %s' % (arg, name, meta['type']) |
752 ) | |
683 | 753 |
684 if 'example' not in meta: | 754 if 'example' not in meta: |
685 raise error.ProgrammingError('%s argument for command %s does not ' | 755 raise error.ProgrammingError( |
686 'declare example field' % (arg, name)) | 756 '%s argument for command %s does not ' |
757 'declare example field' % (arg, name) | |
758 ) | |
687 | 759 |
688 meta['required'] = 'default' not in meta | 760 meta['required'] = 'default' not in meta |
689 | 761 |
690 meta.setdefault('default', lambda: None) | 762 meta.setdefault('default', lambda: None) |
691 meta.setdefault('validvalues', None) | 763 meta.setdefault('validvalues', None) |
692 | 764 |
693 def register(func): | 765 def register(func): |
694 if name in COMMANDS: | 766 if name in COMMANDS: |
695 raise error.ProgrammingError('%s command already registered ' | 767 raise error.ProgrammingError( |
696 'for version 2' % name) | 768 '%s command already registered ' 'for version 2' % name |
769 ) | |
697 | 770 |
698 COMMANDS[name] = wireprototypes.commandentry( | 771 COMMANDS[name] = wireprototypes.commandentry( |
699 func, args=args, transports=transports, permission=permission, | 772 func, |
700 cachekeyfn=cachekeyfn, extracapabilitiesfn=extracapabilitiesfn) | 773 args=args, |
774 transports=transports, | |
775 permission=permission, | |
776 cachekeyfn=cachekeyfn, | |
777 extracapabilitiesfn=extracapabilitiesfn, | |
778 ) | |
701 | 779 |
702 return func | 780 return func |
703 | 781 |
704 return register | 782 return register |
783 | |
705 | 784 |
706 def makecommandcachekeyfn(command, localversion=None, allargs=False): | 785 def makecommandcachekeyfn(command, localversion=None, allargs=False): |
707 """Construct a cache key derivation function with common features. | 786 """Construct a cache key derivation function with common features. |
708 | 787 |
709 By default, the cache key is a hash of: | 788 By default, the cache key is a hash of: |
775 | 854 |
776 return pycompat.sysbytes(hasher.hexdigest()) | 855 return pycompat.sysbytes(hasher.hexdigest()) |
777 | 856 |
778 return cachekeyfn | 857 return cachekeyfn |
779 | 858 |
780 def makeresponsecacher(repo, proto, command, args, objencoderfn, | 859 |
781 redirecttargets, redirecthashes): | 860 def makeresponsecacher( |
861 repo, proto, command, args, objencoderfn, redirecttargets, redirecthashes | |
862 ): | |
782 """Construct a cacher for a cacheable command. | 863 """Construct a cacher for a cacheable command. |
783 | 864 |
784 Returns an ``iwireprotocolcommandcacher`` instance. | 865 Returns an ``iwireprotocolcommandcacher`` instance. |
785 | 866 |
786 Extensions can monkeypatch this function to provide custom caching | 867 Extensions can monkeypatch this function to provide custom caching |
787 backends. | 868 backends. |
788 """ | 869 """ |
789 return None | 870 return None |
790 | 871 |
872 | |
791 def resolvenodes(repo, revisions): | 873 def resolvenodes(repo, revisions): |
792 """Resolve nodes from a revisions specifier data structure.""" | 874 """Resolve nodes from a revisions specifier data structure.""" |
793 cl = repo.changelog | 875 cl = repo.changelog |
794 clhasnode = cl.hasnode | 876 clhasnode = cl.hasnode |
795 | 877 |
796 seen = set() | 878 seen = set() |
797 nodes = [] | 879 nodes = [] |
798 | 880 |
799 if not isinstance(revisions, list): | 881 if not isinstance(revisions, list): |
800 raise error.WireprotoCommandError('revisions must be defined as an ' | 882 raise error.WireprotoCommandError( |
801 'array') | 883 'revisions must be defined as an ' 'array' |
884 ) | |
802 | 885 |
803 for spec in revisions: | 886 for spec in revisions: |
804 if b'type' not in spec: | 887 if b'type' not in spec: |
805 raise error.WireprotoCommandError( | 888 raise error.WireprotoCommandError( |
806 'type key not present in revision specifier') | 889 'type key not present in revision specifier' |
890 ) | |
807 | 891 |
808 typ = spec[b'type'] | 892 typ = spec[b'type'] |
809 | 893 |
810 if typ == b'changesetexplicit': | 894 if typ == b'changesetexplicit': |
811 if b'nodes' not in spec: | 895 if b'nodes' not in spec: |
812 raise error.WireprotoCommandError( | 896 raise error.WireprotoCommandError( |
813 'nodes key not present in changesetexplicit revision ' | 897 'nodes key not present in changesetexplicit revision ' |
814 'specifier') | 898 'specifier' |
899 ) | |
815 | 900 |
816 for node in spec[b'nodes']: | 901 for node in spec[b'nodes']: |
817 if node not in seen: | 902 if node not in seen: |
818 nodes.append(node) | 903 nodes.append(node) |
819 seen.add(node) | 904 seen.add(node) |
821 elif typ == b'changesetexplicitdepth': | 906 elif typ == b'changesetexplicitdepth': |
822 for key in (b'nodes', b'depth'): | 907 for key in (b'nodes', b'depth'): |
823 if key not in spec: | 908 if key not in spec: |
824 raise error.WireprotoCommandError( | 909 raise error.WireprotoCommandError( |
825 '%s key not present in changesetexplicitdepth revision ' | 910 '%s key not present in changesetexplicitdepth revision ' |
826 'specifier', (key,)) | 911 'specifier', |
827 | 912 (key,), |
828 for rev in repo.revs(b'ancestors(%ln, %s)', spec[b'nodes'], | 913 ) |
829 spec[b'depth'] - 1): | 914 |
915 for rev in repo.revs( | |
916 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1 | |
917 ): | |
830 node = cl.node(rev) | 918 node = cl.node(rev) |
831 | 919 |
832 if node not in seen: | 920 if node not in seen: |
833 nodes.append(node) | 921 nodes.append(node) |
834 seen.add(node) | 922 seen.add(node) |
836 elif typ == b'changesetdagrange': | 924 elif typ == b'changesetdagrange': |
837 for key in (b'roots', b'heads'): | 925 for key in (b'roots', b'heads'): |
838 if key not in spec: | 926 if key not in spec: |
839 raise error.WireprotoCommandError( | 927 raise error.WireprotoCommandError( |
840 '%s key not present in changesetdagrange revision ' | 928 '%s key not present in changesetdagrange revision ' |
841 'specifier', (key,)) | 929 'specifier', |
930 (key,), | |
931 ) | |
842 | 932 |
843 if not spec[b'heads']: | 933 if not spec[b'heads']: |
844 raise error.WireprotoCommandError( | 934 raise error.WireprotoCommandError( |
845 'heads key in changesetdagrange cannot be empty') | 935 'heads key in changesetdagrange cannot be empty' |
936 ) | |
846 | 937 |
847 if spec[b'roots']: | 938 if spec[b'roots']: |
848 common = [n for n in spec[b'roots'] if clhasnode(n)] | 939 common = [n for n in spec[b'roots'] if clhasnode(n)] |
849 else: | 940 else: |
850 common = [nullid] | 941 common = [nullid] |
854 nodes.append(n) | 945 nodes.append(n) |
855 seen.add(n) | 946 seen.add(n) |
856 | 947 |
857 else: | 948 else: |
858 raise error.WireprotoCommandError( | 949 raise error.WireprotoCommandError( |
859 'unknown revision specifier type: %s', (typ,)) | 950 'unknown revision specifier type: %s', (typ,) |
951 ) | |
860 | 952 |
861 return nodes | 953 return nodes |
954 | |
862 | 955 |
863 @wireprotocommand('branchmap', permission='pull') | 956 @wireprotocommand('branchmap', permission='pull') |
864 def branchmapv2(repo, proto): | 957 def branchmapv2(repo, proto): |
865 yield {encoding.fromlocal(k): v | 958 yield {encoding.fromlocal(k): v for k, v in repo.branchmap().iteritems()} |
866 for k, v in repo.branchmap().iteritems()} | 959 |
867 | 960 |
868 @wireprotocommand('capabilities', permission='pull') | 961 @wireprotocommand('capabilities', permission='pull') |
869 def capabilitiesv2(repo, proto): | 962 def capabilitiesv2(repo, proto): |
870 yield _capabilitiesv2(repo, proto) | 963 yield _capabilitiesv2(repo, proto) |
964 | |
871 | 965 |
872 @wireprotocommand( | 966 @wireprotocommand( |
873 'changesetdata', | 967 'changesetdata', |
874 args={ | 968 args={ |
875 'revisions': { | 969 'revisions': { |
876 'type': 'list', | 970 'type': 'list', |
877 'example': [{ | 971 'example': [ |
878 b'type': b'changesetexplicit', | 972 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} |
879 b'nodes': [b'abcdef...'], | 973 ], |
880 }], | |
881 }, | 974 }, |
882 'fields': { | 975 'fields': { |
883 'type': 'set', | 976 'type': 'set', |
884 'default': set, | 977 'default': set, |
885 'example': {b'parents', b'revision'}, | 978 'example': {b'parents', b'revision'}, |
886 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, | 979 'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'}, |
887 }, | 980 }, |
888 }, | 981 }, |
889 permission='pull') | 982 permission='pull', |
983 ) | |
890 def changesetdata(repo, proto, revisions, fields): | 984 def changesetdata(repo, proto, revisions, fields): |
891 # TODO look for unknown fields and abort when they can't be serviced. | 985 # TODO look for unknown fields and abort when they can't be serviced. |
892 # This could probably be validated by dispatcher using validvalues. | 986 # This could probably be validated by dispatcher using validvalues. |
893 | 987 |
894 cl = repo.changelog | 988 cl = repo.changelog |
961 yield { | 1055 yield { |
962 b'node': node, | 1056 b'node': node, |
963 b'bookmarks': sorted(marks), | 1057 b'bookmarks': sorted(marks), |
964 } | 1058 } |
965 | 1059 |
1060 | |
966 class FileAccessError(Exception): | 1061 class FileAccessError(Exception): |
967 """Represents an error accessing a specific file.""" | 1062 """Represents an error accessing a specific file.""" |
968 | 1063 |
969 def __init__(self, path, msg, args): | 1064 def __init__(self, path, msg, args): |
970 self.path = path | 1065 self.path = path |
971 self.msg = msg | 1066 self.msg = msg |
972 self.args = args | 1067 self.args = args |
1068 | |
973 | 1069 |
974 def getfilestore(repo, proto, path): | 1070 def getfilestore(repo, proto, path): |
975 """Obtain a file storage object for use with wire protocol. | 1071 """Obtain a file storage object for use with wire protocol. |
976 | 1072 |
977 Exists as a standalone function so extensions can monkeypatch to add | 1073 Exists as a standalone function so extensions can monkeypatch to add |
983 | 1079 |
984 if not len(fl): | 1080 if not len(fl): |
985 raise FileAccessError(path, 'unknown file: %s', (path,)) | 1081 raise FileAccessError(path, 'unknown file: %s', (path,)) |
986 | 1082 |
987 return fl | 1083 return fl |
1084 | |
988 | 1085 |
989 def emitfilerevisions(repo, path, revisions, linknodes, fields): | 1086 def emitfilerevisions(repo, path, revisions, linknodes, fields): |
990 for revision in revisions: | 1087 for revision in revisions: |
991 d = { | 1088 d = { |
992 b'node': revision.node, | 1089 b'node': revision.node, |
1016 yield d | 1113 yield d |
1017 | 1114 |
1018 for extra in followingdata: | 1115 for extra in followingdata: |
1019 yield extra | 1116 yield extra |
1020 | 1117 |
1118 | |
1021 def makefilematcher(repo, pathfilter): | 1119 def makefilematcher(repo, pathfilter): |
1022 """Construct a matcher from a path filter dict.""" | 1120 """Construct a matcher from a path filter dict.""" |
1023 | 1121 |
1024 # Validate values. | 1122 # Validate values. |
1025 if pathfilter: | 1123 if pathfilter: |
1026 for key in (b'include', b'exclude'): | 1124 for key in (b'include', b'exclude'): |
1027 for pattern in pathfilter.get(key, []): | 1125 for pattern in pathfilter.get(key, []): |
1028 if not pattern.startswith((b'path:', b'rootfilesin:')): | 1126 if not pattern.startswith((b'path:', b'rootfilesin:')): |
1029 raise error.WireprotoCommandError( | 1127 raise error.WireprotoCommandError( |
1030 '%s pattern must begin with `path:` or `rootfilesin:`; ' | 1128 '%s pattern must begin with `path:` or `rootfilesin:`; ' |
1031 'got %s', (key, pattern)) | 1129 'got %s', |
1130 (key, pattern), | |
1131 ) | |
1032 | 1132 |
1033 if pathfilter: | 1133 if pathfilter: |
1034 matcher = matchmod.match(repo.root, b'', | 1134 matcher = matchmod.match( |
1035 include=pathfilter.get(b'include', []), | 1135 repo.root, |
1036 exclude=pathfilter.get(b'exclude', [])) | 1136 b'', |
1137 include=pathfilter.get(b'include', []), | |
1138 exclude=pathfilter.get(b'exclude', []), | |
1139 ) | |
1037 else: | 1140 else: |
1038 matcher = matchmod.match(repo.root, b'') | 1141 matcher = matchmod.match(repo.root, b'') |
1039 | 1142 |
1040 # Requested patterns could include files not in the local store. So | 1143 # Requested patterns could include files not in the local store. So |
1041 # filter those out. | 1144 # filter those out. |
1042 return repo.narrowmatch(matcher) | 1145 return repo.narrowmatch(matcher) |
1146 | |
1043 | 1147 |
1044 @wireprotocommand( | 1148 @wireprotocommand( |
1045 'filedata', | 1149 'filedata', |
1046 args={ | 1150 args={ |
1047 'haveparents': { | 1151 'haveparents': { |
1048 'type': 'bool', | 1152 'type': 'bool', |
1049 'default': lambda: False, | 1153 'default': lambda: False, |
1050 'example': True, | 1154 'example': True, |
1051 }, | 1155 }, |
1052 'nodes': { | 1156 'nodes': {'type': 'list', 'example': [b'0123456...'],}, |
1053 'type': 'list', | |
1054 'example': [b'0123456...'], | |
1055 }, | |
1056 'fields': { | 1157 'fields': { |
1057 'type': 'set', | 1158 'type': 'set', |
1058 'default': set, | 1159 'default': set, |
1059 'example': {b'parents', b'revision'}, | 1160 'example': {b'parents', b'revision'}, |
1060 'validvalues': {b'parents', b'revision', b'linknode'}, | 1161 'validvalues': {b'parents', b'revision', b'linknode'}, |
1061 }, | 1162 }, |
1062 'path': { | 1163 'path': {'type': 'bytes', 'example': b'foo.txt',}, |
1063 'type': 'bytes', | |
1064 'example': b'foo.txt', | |
1065 } | |
1066 }, | 1164 }, |
1067 permission='pull', | 1165 permission='pull', |
1068 # TODO censoring a file revision won't invalidate the cache. | 1166 # TODO censoring a file revision won't invalidate the cache. |
1069 # Figure out a way to take censoring into account when deriving | 1167 # Figure out a way to take censoring into account when deriving |
1070 # the cache key. | 1168 # the cache key. |
1071 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True)) | 1169 cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True), |
1170 ) | |
1072 def filedata(repo, proto, haveparents, nodes, fields, path): | 1171 def filedata(repo, proto, haveparents, nodes, fields, path): |
1073 # TODO this API allows access to file revisions that are attached to | 1172 # TODO this API allows access to file revisions that are attached to |
1074 # secret changesets. filesdata does not have this problem. Maybe this | 1173 # secret changesets. filesdata does not have this problem. Maybe this |
1075 # API should be deleted? | 1174 # API should be deleted? |
1076 | 1175 |
1086 # Validate requested nodes. | 1185 # Validate requested nodes. |
1087 for node in nodes: | 1186 for node in nodes: |
1088 try: | 1187 try: |
1089 store.rev(node) | 1188 store.rev(node) |
1090 except error.LookupError: | 1189 except error.LookupError: |
1091 raise error.WireprotoCommandError('unknown file node: %s', | 1190 raise error.WireprotoCommandError( |
1092 (hex(node),)) | 1191 'unknown file node: %s', (hex(node),) |
1192 ) | |
1093 | 1193 |
1094 # TODO by creating the filectx against a specific file revision | 1194 # TODO by creating the filectx against a specific file revision |
1095 # instead of changeset, linkrev() is always used. This is wrong for | 1195 # instead of changeset, linkrev() is always used. This is wrong for |
1096 # cases where linkrev() may refer to a hidden changeset. But since this | 1196 # cases where linkrev() may refer to a hidden changeset. But since this |
1097 # API doesn't know anything about changesets, we're not sure how to | 1197 # API doesn't know anything about changesets, we're not sure how to |
1098 # disambiguate the linknode. Perhaps we should delete this API? | 1198 # disambiguate the linknode. Perhaps we should delete this API? |
1099 fctx = repo.filectx(path, fileid=node) | 1199 fctx = repo.filectx(path, fileid=node) |
1100 linknodes[node] = clnode(fctx.introrev()) | 1200 linknodes[node] = clnode(fctx.introrev()) |
1101 | 1201 |
1102 revisions = store.emitrevisions(nodes, | 1202 revisions = store.emitrevisions( |
1103 revisiondata=b'revision' in fields, | 1203 nodes, |
1104 assumehaveparentrevisions=haveparents) | 1204 revisiondata=b'revision' in fields, |
1205 assumehaveparentrevisions=haveparents, | |
1206 ) | |
1105 | 1207 |
1106 yield { | 1208 yield { |
1107 b'totalitems': len(nodes), | 1209 b'totalitems': len(nodes), |
1108 } | 1210 } |
1109 | 1211 |
1110 for o in emitfilerevisions(repo, path, revisions, linknodes, fields): | 1212 for o in emitfilerevisions(repo, path, revisions, linknodes, fields): |
1111 yield o | 1213 yield o |
1112 | 1214 |
1215 | |
1113 def filesdatacapabilities(repo, proto): | 1216 def filesdatacapabilities(repo, proto): |
1114 batchsize = repo.ui.configint( | 1217 batchsize = repo.ui.configint( |
1115 b'experimental', b'server.filesdata.recommended-batch-size') | 1218 b'experimental', b'server.filesdata.recommended-batch-size' |
1219 ) | |
1116 return { | 1220 return { |
1117 b'recommendedbatchsize': batchsize, | 1221 b'recommendedbatchsize': batchsize, |
1118 } | 1222 } |
1223 | |
1119 | 1224 |
1120 @wireprotocommand( | 1225 @wireprotocommand( |
1121 'filesdata', | 1226 'filesdata', |
1122 args={ | 1227 args={ |
1123 'haveparents': { | 1228 'haveparents': { |
1127 }, | 1232 }, |
1128 'fields': { | 1233 'fields': { |
1129 'type': 'set', | 1234 'type': 'set', |
1130 'default': set, | 1235 'default': set, |
1131 'example': {b'parents', b'revision'}, | 1236 'example': {b'parents', b'revision'}, |
1132 'validvalues': {b'firstchangeset', b'linknode', b'parents', | 1237 'validvalues': { |
1133 b'revision'}, | 1238 b'firstchangeset', |
1239 b'linknode', | |
1240 b'parents', | |
1241 b'revision', | |
1242 }, | |
1134 }, | 1243 }, |
1135 'pathfilter': { | 1244 'pathfilter': { |
1136 'type': 'dict', | 1245 'type': 'dict', |
1137 'default': lambda: None, | 1246 'default': lambda: None, |
1138 'example': {b'include': [b'path:tests']}, | 1247 'example': {b'include': [b'path:tests']}, |
1139 }, | 1248 }, |
1140 'revisions': { | 1249 'revisions': { |
1141 'type': 'list', | 1250 'type': 'list', |
1142 'example': [{ | 1251 'example': [ |
1143 b'type': b'changesetexplicit', | 1252 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],} |
1144 b'nodes': [b'abcdef...'], | 1253 ], |
1145 }], | |
1146 }, | 1254 }, |
1147 }, | 1255 }, |
1148 permission='pull', | 1256 permission='pull', |
1149 # TODO censoring a file revision won't invalidate the cache. | 1257 # TODO censoring a file revision won't invalidate the cache. |
1150 # Figure out a way to take censoring into account when deriving | 1258 # Figure out a way to take censoring into account when deriving |
1151 # the cache key. | 1259 # the cache key. |
1152 cachekeyfn=makecommandcachekeyfn('filesdata', 1, allargs=True), | 1260 cachekeyfn=makecommandcachekeyfn('filesdata', 1, allargs=True), |
1153 extracapabilitiesfn=filesdatacapabilities) | 1261 extracapabilitiesfn=filesdatacapabilities, |
1262 ) | |
1154 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions): | 1263 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions): |
1155 # TODO This should operate on a repo that exposes obsolete changesets. There | 1264 # TODO This should operate on a repo that exposes obsolete changesets. There |
1156 # is a race between a client making a push that obsoletes a changeset and | 1265 # is a race between a client making a push that obsoletes a changeset and |
1157 # another client fetching files data for that changeset. If a client has a | 1266 # another client fetching files data for that changeset. If a client has a |
1158 # changeset, it should probably be allowed to access files data for that | 1267 # changeset, it should probably be allowed to access files data for that |
1191 | 1300 |
1192 fnodes[path].setdefault(fnode, node) | 1301 fnodes[path].setdefault(fnode, node) |
1193 | 1302 |
1194 yield { | 1303 yield { |
1195 b'totalpaths': len(fnodes), | 1304 b'totalpaths': len(fnodes), |
1196 b'totalitems': sum(len(v) for v in fnodes.values()) | 1305 b'totalitems': sum(len(v) for v in fnodes.values()), |
1197 } | 1306 } |
1198 | 1307 |
1199 for path, filenodes in sorted(fnodes.items()): | 1308 for path, filenodes in sorted(fnodes.items()): |
1200 try: | 1309 try: |
1201 store = getfilestore(repo, proto, path) | 1310 store = getfilestore(repo, proto, path) |
1205 yield { | 1314 yield { |
1206 b'path': path, | 1315 b'path': path, |
1207 b'totalitems': len(filenodes), | 1316 b'totalitems': len(filenodes), |
1208 } | 1317 } |
1209 | 1318 |
1210 revisions = store.emitrevisions(filenodes.keys(), | 1319 revisions = store.emitrevisions( |
1211 revisiondata=b'revision' in fields, | 1320 filenodes.keys(), |
1212 assumehaveparentrevisions=haveparents) | 1321 revisiondata=b'revision' in fields, |
1322 assumehaveparentrevisions=haveparents, | |
1323 ) | |
1213 | 1324 |
1214 for o in emitfilerevisions(repo, path, revisions, filenodes, fields): | 1325 for o in emitfilerevisions(repo, path, revisions, filenodes, fields): |
1215 yield o | 1326 yield o |
1327 | |
1216 | 1328 |
1217 @wireprotocommand( | 1329 @wireprotocommand( |
1218 'heads', | 1330 'heads', |
1219 args={ | 1331 args={ |
1220 'publiconly': { | 1332 'publiconly': { |
1221 'type': 'bool', | 1333 'type': 'bool', |
1222 'default': lambda: False, | 1334 'default': lambda: False, |
1223 'example': False, | 1335 'example': False, |
1224 }, | 1336 }, |
1225 }, | 1337 }, |
1226 permission='pull') | 1338 permission='pull', |
1339 ) | |
1227 def headsv2(repo, proto, publiconly): | 1340 def headsv2(repo, proto, publiconly): |
1228 if publiconly: | 1341 if publiconly: |
1229 repo = repo.filtered('immutable') | 1342 repo = repo.filtered('immutable') |
1230 | 1343 |
1231 yield repo.heads() | 1344 yield repo.heads() |
1232 | 1345 |
1346 | |
1233 @wireprotocommand( | 1347 @wireprotocommand( |
1234 'known', | 1348 'known', |
1235 args={ | 1349 args={ |
1236 'nodes': { | 1350 'nodes': {'type': 'list', 'default': list, 'example': [b'deadbeef'],}, |
1237 'type': 'list', | |
1238 'default': list, | |
1239 'example': [b'deadbeef'], | |
1240 }, | |
1241 }, | 1351 }, |
1242 permission='pull') | 1352 permission='pull', |
1353 ) | |
1243 def knownv2(repo, proto, nodes): | 1354 def knownv2(repo, proto, nodes): |
1244 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes)) | 1355 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes)) |
1245 yield result | 1356 yield result |
1246 | 1357 |
1358 | |
1247 @wireprotocommand( | 1359 @wireprotocommand( |
1248 'listkeys', | 1360 'listkeys', |
1249 args={ | 1361 args={'namespace': {'type': 'bytes', 'example': b'ns',},}, |
1250 'namespace': { | 1362 permission='pull', |
1251 'type': 'bytes', | 1363 ) |
1252 'example': b'ns', | |
1253 }, | |
1254 }, | |
1255 permission='pull') | |
1256 def listkeysv2(repo, proto, namespace): | 1364 def listkeysv2(repo, proto, namespace): |
1257 keys = repo.listkeys(encoding.tolocal(namespace)) | 1365 keys = repo.listkeys(encoding.tolocal(namespace)) |
1258 keys = {encoding.fromlocal(k): encoding.fromlocal(v) | 1366 keys = { |
1259 for k, v in keys.iteritems()} | 1367 encoding.fromlocal(k): encoding.fromlocal(v) |
1368 for k, v in keys.iteritems() | |
1369 } | |
1260 | 1370 |
1261 yield keys | 1371 yield keys |
1372 | |
1262 | 1373 |
1263 @wireprotocommand( | 1374 @wireprotocommand( |
1264 'lookup', | 1375 'lookup', |
1265 args={ | 1376 args={'key': {'type': 'bytes', 'example': b'foo',},}, |
1266 'key': { | 1377 permission='pull', |
1267 'type': 'bytes', | 1378 ) |
1268 'example': b'foo', | |
1269 }, | |
1270 }, | |
1271 permission='pull') | |
1272 def lookupv2(repo, proto, key): | 1379 def lookupv2(repo, proto, key): |
1273 key = encoding.tolocal(key) | 1380 key = encoding.tolocal(key) |
1274 | 1381 |
1275 # TODO handle exception. | 1382 # TODO handle exception. |
1276 node = repo.lookup(key) | 1383 node = repo.lookup(key) |
1277 | 1384 |
1278 yield node | 1385 yield node |
1279 | 1386 |
1387 | |
1280 def manifestdatacapabilities(repo, proto): | 1388 def manifestdatacapabilities(repo, proto): |
1281 batchsize = repo.ui.configint( | 1389 batchsize = repo.ui.configint( |
1282 b'experimental', b'server.manifestdata.recommended-batch-size') | 1390 b'experimental', b'server.manifestdata.recommended-batch-size' |
1391 ) | |
1283 | 1392 |
1284 return { | 1393 return { |
1285 b'recommendedbatchsize': batchsize, | 1394 b'recommendedbatchsize': batchsize, |
1286 } | 1395 } |
1287 | 1396 |
1397 | |
1288 @wireprotocommand( | 1398 @wireprotocommand( |
1289 'manifestdata', | 1399 'manifestdata', |
1290 args={ | 1400 args={ |
1291 'nodes': { | 1401 'nodes': {'type': 'list', 'example': [b'0123456...'],}, |
1292 'type': 'list', | |
1293 'example': [b'0123456...'], | |
1294 }, | |
1295 'haveparents': { | 1402 'haveparents': { |
1296 'type': 'bool', | 1403 'type': 'bool', |
1297 'default': lambda: False, | 1404 'default': lambda: False, |
1298 'example': True, | 1405 'example': True, |
1299 }, | 1406 }, |
1301 'type': 'set', | 1408 'type': 'set', |
1302 'default': set, | 1409 'default': set, |
1303 'example': {b'parents', b'revision'}, | 1410 'example': {b'parents', b'revision'}, |
1304 'validvalues': {b'parents', b'revision'}, | 1411 'validvalues': {b'parents', b'revision'}, |
1305 }, | 1412 }, |
1306 'tree': { | 1413 'tree': {'type': 'bytes', 'example': b'',}, |
1307 'type': 'bytes', | |
1308 'example': b'', | |
1309 }, | |
1310 }, | 1414 }, |
1311 permission='pull', | 1415 permission='pull', |
1312 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True), | 1416 cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True), |
1313 extracapabilitiesfn=manifestdatacapabilities) | 1417 extracapabilitiesfn=manifestdatacapabilities, |
1418 ) | |
1314 def manifestdata(repo, proto, haveparents, nodes, fields, tree): | 1419 def manifestdata(repo, proto, haveparents, nodes, fields, tree): |
1315 store = repo.manifestlog.getstorage(tree) | 1420 store = repo.manifestlog.getstorage(tree) |
1316 | 1421 |
1317 # Validate the node is known and abort on unknown revisions. | 1422 # Validate the node is known and abort on unknown revisions. |
1318 for node in nodes: | 1423 for node in nodes: |
1319 try: | 1424 try: |
1320 store.rev(node) | 1425 store.rev(node) |
1321 except error.LookupError: | 1426 except error.LookupError: |
1322 raise error.WireprotoCommandError( | 1427 raise error.WireprotoCommandError('unknown node: %s', (node,)) |
1323 'unknown node: %s', (node,)) | 1428 |
1324 | 1429 revisions = store.emitrevisions( |
1325 revisions = store.emitrevisions(nodes, | 1430 nodes, |
1326 revisiondata=b'revision' in fields, | 1431 revisiondata=b'revision' in fields, |
1327 assumehaveparentrevisions=haveparents) | 1432 assumehaveparentrevisions=haveparents, |
1433 ) | |
1328 | 1434 |
1329 yield { | 1435 yield { |
1330 b'totalitems': len(nodes), | 1436 b'totalitems': len(nodes), |
1331 } | 1437 } |
1332 | 1438 |
1356 yield d | 1462 yield d |
1357 | 1463 |
1358 for extra in followingdata: | 1464 for extra in followingdata: |
1359 yield extra | 1465 yield extra |
1360 | 1466 |
1467 | |
1361 @wireprotocommand( | 1468 @wireprotocommand( |
1362 'pushkey', | 1469 'pushkey', |
1363 args={ | 1470 args={ |
1364 'namespace': { | 1471 'namespace': {'type': 'bytes', 'example': b'ns',}, |
1365 'type': 'bytes', | 1472 'key': {'type': 'bytes', 'example': b'key',}, |
1366 'example': b'ns', | 1473 'old': {'type': 'bytes', 'example': b'old',}, |
1367 }, | 1474 'new': {'type': 'bytes', 'example': 'new',}, |
1368 'key': { | |
1369 'type': 'bytes', | |
1370 'example': b'key', | |
1371 }, | |
1372 'old': { | |
1373 'type': 'bytes', | |
1374 'example': b'old', | |
1375 }, | |
1376 'new': { | |
1377 'type': 'bytes', | |
1378 'example': 'new', | |
1379 }, | |
1380 }, | 1475 }, |
1381 permission='push') | 1476 permission='push', |
1477 ) | |
1382 def pushkeyv2(repo, proto, namespace, key, old, new): | 1478 def pushkeyv2(repo, proto, namespace, key, old, new): |
1383 # TODO handle ui output redirection | 1479 # TODO handle ui output redirection |
1384 yield repo.pushkey(encoding.tolocal(namespace), | 1480 yield repo.pushkey( |
1385 encoding.tolocal(key), | 1481 encoding.tolocal(namespace), |
1386 encoding.tolocal(old), | 1482 encoding.tolocal(key), |
1387 encoding.tolocal(new)) | 1483 encoding.tolocal(old), |
1484 encoding.tolocal(new), | |
1485 ) | |
1388 | 1486 |
1389 | 1487 |
1390 @wireprotocommand( | 1488 @wireprotocommand( |
1391 'rawstorefiledata', | 1489 'rawstorefiledata', |
1392 args={ | 1490 args={ |
1393 'files': { | 1491 'files': {'type': 'list', 'example': [b'changelog', b'manifestlog'],}, |
1394 'type': 'list', | |
1395 'example': [b'changelog', b'manifestlog'], | |
1396 }, | |
1397 'pathfilter': { | 1492 'pathfilter': { |
1398 'type': 'list', | 1493 'type': 'list', |
1399 'default': lambda: None, | 1494 'default': lambda: None, |
1400 'example': {b'include': [b'path:tests']}, | 1495 'example': {b'include': [b'path:tests']}, |
1401 }, | 1496 }, |
1402 }, | 1497 }, |
1403 permission='pull') | 1498 permission='pull', |
1499 ) | |
1404 def rawstorefiledata(repo, proto, files, pathfilter): | 1500 def rawstorefiledata(repo, proto, files, pathfilter): |
1405 if not streamclone.allowservergeneration(repo): | 1501 if not streamclone.allowservergeneration(repo): |
1406 raise error.WireprotoCommandError(b'stream clone is disabled') | 1502 raise error.WireprotoCommandError(b'stream clone is disabled') |
1407 | 1503 |
1408 # TODO support dynamically advertising what store files "sets" are | 1504 # TODO support dynamically advertising what store files "sets" are |
1410 files = set(files) | 1506 files = set(files) |
1411 allowedfiles = {b'changelog', b'manifestlog'} | 1507 allowedfiles = {b'changelog', b'manifestlog'} |
1412 | 1508 |
1413 unsupported = files - allowedfiles | 1509 unsupported = files - allowedfiles |
1414 if unsupported: | 1510 if unsupported: |
1415 raise error.WireprotoCommandError(b'unknown file type: %s', | 1511 raise error.WireprotoCommandError( |
1416 (b', '.join(sorted(unsupported)),)) | 1512 b'unknown file type: %s', (b', '.join(sorted(unsupported)),) |
1513 ) | |
1417 | 1514 |
1418 with repo.lock(): | 1515 with repo.lock(): |
1419 topfiles = list(repo.store.topfiles()) | 1516 topfiles = list(repo.store.topfiles()) |
1420 | 1517 |
1421 sendfiles = [] | 1518 sendfiles = [] |
1451 def getfiledata(): | 1548 def getfiledata(): |
1452 with repo.svfs(name, 'rb', auditpath=False) as fh: | 1549 with repo.svfs(name, 'rb', auditpath=False) as fh: |
1453 for chunk in util.filechunkiter(fh, limit=size): | 1550 for chunk in util.filechunkiter(fh, limit=size): |
1454 yield chunk | 1551 yield chunk |
1455 | 1552 |
1456 yield wireprototypes.indefinitebytestringresponse( | 1553 yield wireprototypes.indefinitebytestringresponse(getfiledata()) |
1457 getfiledata()) |