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())