##// END OF EJS Templates
pytype: stop excluding wireprotov2server.py...
Matt Harbison -
r49311:cfb4f1de default
parent child Browse files
Show More
@@ -1,1613 +1,1617 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import collections
10 10 import contextlib
11 11
12 12 from .i18n import _
13 13 from .node import hex
14 14 from . import (
15 15 discovery,
16 16 encoding,
17 17 error,
18 18 match as matchmod,
19 19 narrowspec,
20 20 pycompat,
21 21 streamclone,
22 22 templatefilters,
23 23 util,
24 24 wireprotoframing,
25 25 wireprototypes,
26 26 )
27 27 from .interfaces import util as interfaceutil
28 28 from .utils import (
29 29 cborutil,
30 30 hashutil,
31 31 stringutil,
32 32 )
33 33
34 34 FRAMINGTYPE = b'application/mercurial-exp-framing-0006'
35 35
36 36 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
37 37
38 38 COMMANDS = wireprototypes.commanddict()
39 39
40 40 # Value inserted into cache key computation function. Change the value to
41 41 # force new cache keys for every command request. This should be done when
42 42 # there is a change to how caching works, etc.
43 43 GLOBAL_CACHE_VERSION = 1
44 44
45 45
46 46 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
47 47 from .hgweb import common as hgwebcommon
48 48
49 49 # URL space looks like: <permissions>/<command>, where <permission> can
50 50 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
51 51
52 52 # Root URL does nothing meaningful... yet.
53 53 if not urlparts:
54 54 res.status = b'200 OK'
55 55 res.headers[b'Content-Type'] = b'text/plain'
56 56 res.setbodybytes(_(b'HTTP version 2 API handler'))
57 57 return
58 58
59 59 if len(urlparts) == 1:
60 60 res.status = b'404 Not Found'
61 61 res.headers[b'Content-Type'] = b'text/plain'
62 62 res.setbodybytes(
63 63 _(b'do not know how to process %s\n') % req.dispatchpath
64 64 )
65 65 return
66 66
67 67 permission, command = urlparts[0:2]
68 68
69 69 if permission not in (b'ro', b'rw'):
70 70 res.status = b'404 Not Found'
71 71 res.headers[b'Content-Type'] = b'text/plain'
72 72 res.setbodybytes(_(b'unknown permission: %s') % permission)
73 73 return
74 74
75 75 if req.method != b'POST':
76 76 res.status = b'405 Method Not Allowed'
77 77 res.headers[b'Allow'] = b'POST'
78 78 res.setbodybytes(_(b'commands require POST requests'))
79 79 return
80 80
81 81 # At some point we'll want to use our own API instead of recycling the
82 82 # behavior of version 1 of the wire protocol...
83 83 # TODO return reasonable responses - not responses that overload the
84 84 # HTTP status line message for error reporting.
85 85 try:
86 86 checkperm(rctx, req, b'pull' if permission == b'ro' else b'push')
87 87 except hgwebcommon.ErrorResponse as e:
88 88 res.status = hgwebcommon.statusmessage(
89 89 e.code, stringutil.forcebytestr(e)
90 90 )
91 91 for k, v in e.headers:
92 92 res.headers[k] = v
93 93 res.setbodybytes(b'permission denied')
94 94 return
95 95
96 96 # We have a special endpoint to reflect the request back at the client.
97 97 if command == b'debugreflect':
98 98 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
99 99 return
100 100
101 101 # Extra commands that we handle that aren't really wire protocol
102 102 # commands. Think extra hard before making this hackery available to
103 103 # extension.
104 104 extracommands = {b'multirequest'}
105 105
106 106 if command not in COMMANDS and command not in extracommands:
107 107 res.status = b'404 Not Found'
108 108 res.headers[b'Content-Type'] = b'text/plain'
109 109 res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command)
110 110 return
111 111
112 112 repo = rctx.repo
113 113 ui = repo.ui
114 114
115 115 proto = httpv2protocolhandler(req, ui)
116 116
117 117 if (
118 118 not COMMANDS.commandavailable(command, proto)
119 119 and command not in extracommands
120 120 ):
121 121 res.status = b'404 Not Found'
122 122 res.headers[b'Content-Type'] = b'text/plain'
123 123 res.setbodybytes(_(b'invalid wire protocol command: %s') % command)
124 124 return
125 125
126 126 # TODO consider cases where proxies may add additional Accept headers.
127 127 if req.headers.get(b'Accept') != FRAMINGTYPE:
128 128 res.status = b'406 Not Acceptable'
129 129 res.headers[b'Content-Type'] = b'text/plain'
130 130 res.setbodybytes(
131 131 _(b'client MUST specify Accept header with value: %s\n')
132 132 % FRAMINGTYPE
133 133 )
134 134 return
135 135
136 136 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
137 137 res.status = b'415 Unsupported Media Type'
138 138 # TODO we should send a response with appropriate media type,
139 139 # since client does Accept it.
140 140 res.headers[b'Content-Type'] = b'text/plain'
141 141 res.setbodybytes(
142 142 _(b'client MUST send Content-Type header with value: %s\n')
143 143 % FRAMINGTYPE
144 144 )
145 145 return
146 146
147 147 _processhttpv2request(ui, repo, req, res, permission, command, proto)
148 148
149 149
150 150 def _processhttpv2reflectrequest(ui, repo, req, res):
151 151 """Reads unified frame protocol request and dumps out state to client.
152 152
153 153 This special endpoint can be used to help debug the wire protocol.
154 154
155 155 Instead of routing the request through the normal dispatch mechanism,
156 156 we instead read all frames, decode them, and feed them into our state
157 157 tracker. We then dump the log of all that activity back out to the
158 158 client.
159 159 """
160 160 # Reflection APIs have a history of being abused, accidentally disclosing
161 161 # sensitive data, etc. So we have a config knob.
162 162 if not ui.configbool(b'experimental', b'web.api.debugreflect'):
163 163 res.status = b'404 Not Found'
164 164 res.headers[b'Content-Type'] = b'text/plain'
165 165 res.setbodybytes(_(b'debugreflect service not available'))
166 166 return
167 167
168 168 # We assume we have a unified framing protocol request body.
169 169
170 170 reactor = wireprotoframing.serverreactor(ui)
171 171 states = []
172 172
173 173 while True:
174 174 frame = wireprotoframing.readframe(req.bodyfh)
175 175
176 176 if not frame:
177 177 states.append(b'received: <no frame>')
178 178 break
179 179
180 180 states.append(
181 181 b'received: %d %d %d %s'
182 182 % (frame.typeid, frame.flags, frame.requestid, frame.payload)
183 183 )
184 184
185 185 action, meta = reactor.onframerecv(frame)
186 186 states.append(templatefilters.json((action, meta)))
187 187
188 188 action, meta = reactor.oninputeof()
189 189 meta[b'action'] = action
190 190 states.append(templatefilters.json(meta))
191 191
192 192 res.status = b'200 OK'
193 193 res.headers[b'Content-Type'] = b'text/plain'
194 194 res.setbodybytes(b'\n'.join(states))
195 195
196 196
197 197 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
198 198 """Post-validation handler for HTTPv2 requests.
199 199
200 200 Called when the HTTP request contains unified frame-based protocol
201 201 frames for evaluation.
202 202 """
203 203 # TODO Some HTTP clients are full duplex and can receive data before
204 204 # the entire request is transmitted. Figure out a way to indicate support
205 205 # for that so we can opt into full duplex mode.
206 206 reactor = wireprotoframing.serverreactor(ui, deferoutput=True)
207 207 seencommand = False
208 208
209 209 outstream = None
210 210
211 211 while True:
212 212 frame = wireprotoframing.readframe(req.bodyfh)
213 213 if not frame:
214 214 break
215 215
216 216 action, meta = reactor.onframerecv(frame)
217 217
218 218 if action == b'wantframe':
219 219 # Need more data before we can do anything.
220 220 continue
221 221 elif action == b'runcommand':
222 222 # Defer creating output stream because we need to wait for
223 223 # protocol settings frames so proper encoding can be applied.
224 224 if not outstream:
225 225 outstream = reactor.makeoutputstream()
226 226
227 227 sentoutput = _httpv2runcommand(
228 228 ui,
229 229 repo,
230 230 req,
231 231 res,
232 232 authedperm,
233 233 reqcommand,
234 234 reactor,
235 235 outstream,
236 236 meta,
237 237 issubsequent=seencommand,
238 238 )
239 239
240 240 if sentoutput:
241 241 return
242 242
243 243 seencommand = True
244 244
245 245 elif action == b'error':
246 246 # TODO define proper error mechanism.
247 247 res.status = b'200 OK'
248 248 res.headers[b'Content-Type'] = b'text/plain'
249 249 res.setbodybytes(meta[b'message'] + b'\n')
250 250 return
251 251 else:
252 252 raise error.ProgrammingError(
253 253 b'unhandled action from frame processor: %s' % action
254 254 )
255 255
256 256 action, meta = reactor.oninputeof()
257 257 if action == b'sendframes':
258 258 # We assume we haven't started sending the response yet. If we're
259 259 # wrong, the response type will raise an exception.
260 260 res.status = b'200 OK'
261 261 res.headers[b'Content-Type'] = FRAMINGTYPE
262 262 res.setbodygen(meta[b'framegen'])
263 263 elif action == b'noop':
264 264 pass
265 265 else:
266 266 raise error.ProgrammingError(
267 267 b'unhandled action from frame processor: %s' % action
268 268 )
269 269
270 270
271 271 def _httpv2runcommand(
272 272 ui,
273 273 repo,
274 274 req,
275 275 res,
276 276 authedperm,
277 277 reqcommand,
278 278 reactor,
279 279 outstream,
280 280 command,
281 281 issubsequent,
282 282 ):
283 283 """Dispatch a wire protocol command made from HTTPv2 requests.
284 284
285 285 The authenticated permission (``authedperm``) along with the original
286 286 command from the URL (``reqcommand``) are passed in.
287 287 """
288 288 # We already validated that the session has permissions to perform the
289 289 # actions in ``authedperm``. In the unified frame protocol, the canonical
290 290 # command to run is expressed in a frame. However, the URL also requested
291 291 # to run a specific command. We need to be careful that the command we
292 292 # run doesn't have permissions requirements greater than what was granted
293 293 # by ``authedperm``.
294 294 #
295 295 # Our rule for this is we only allow one command per HTTP request and
296 296 # that command must match the command in the URL. However, we make
297 297 # an exception for the ``multirequest`` URL. This URL is allowed to
298 298 # execute multiple commands. We double check permissions of each command
299 299 # as it is invoked to ensure there is no privilege escalation.
300 300 # TODO consider allowing multiple commands to regular command URLs
301 301 # iff each command is the same.
302 302
303 303 proto = httpv2protocolhandler(req, ui, args=command[b'args'])
304 304
305 305 if reqcommand == b'multirequest':
306 306 if not COMMANDS.commandavailable(command[b'command'], proto):
307 307 # TODO proper error mechanism
308 308 res.status = b'200 OK'
309 309 res.headers[b'Content-Type'] = b'text/plain'
310 310 res.setbodybytes(
311 311 _(b'wire protocol command not available: %s')
312 312 % command[b'command']
313 313 )
314 314 return True
315 315
316 316 # TODO don't use assert here, since it may be elided by -O.
317 317 assert authedperm in (b'ro', b'rw')
318 318 wirecommand = COMMANDS[command[b'command']]
319 319 assert wirecommand.permission in (b'push', b'pull')
320 320
321 321 if authedperm == b'ro' and wirecommand.permission != b'pull':
322 322 # TODO proper error mechanism
323 323 res.status = b'403 Forbidden'
324 324 res.headers[b'Content-Type'] = b'text/plain'
325 325 res.setbodybytes(
326 326 _(b'insufficient permissions to execute command: %s')
327 327 % command[b'command']
328 328 )
329 329 return True
330 330
331 331 # TODO should we also call checkperm() here? Maybe not if we're going
332 332 # to overhaul that API. The granted scope from the URL check should
333 333 # be good enough.
334 334
335 335 else:
336 336 # Don't allow multiple commands outside of ``multirequest`` URL.
337 337 if issubsequent:
338 338 # TODO proper error mechanism
339 339 res.status = b'200 OK'
340 340 res.headers[b'Content-Type'] = b'text/plain'
341 341 res.setbodybytes(
342 342 _(b'multiple commands cannot be issued to this URL')
343 343 )
344 344 return True
345 345
346 346 if reqcommand != command[b'command']:
347 347 # TODO define proper error mechanism
348 348 res.status = b'200 OK'
349 349 res.headers[b'Content-Type'] = b'text/plain'
350 350 res.setbodybytes(_(b'command in frame must match command in URL'))
351 351 return True
352 352
353 353 res.status = b'200 OK'
354 354 res.headers[b'Content-Type'] = FRAMINGTYPE
355 355
356 356 try:
357 357 objs = dispatch(repo, proto, command[b'command'], command[b'redirect'])
358 358
359 359 action, meta = reactor.oncommandresponsereadyobjects(
360 360 outstream, command[b'requestid'], objs
361 361 )
362 362
363 363 except error.WireprotoCommandError as e:
364 364 action, meta = reactor.oncommanderror(
365 365 outstream, command[b'requestid'], e.message, e.messageargs
366 366 )
367 367
368 368 except Exception as e:
369 369 action, meta = reactor.onservererror(
370 370 outstream,
371 371 command[b'requestid'],
372 372 _(b'exception when invoking command: %s')
373 373 % stringutil.forcebytestr(e),
374 374 )
375 375
376 376 if action == b'sendframes':
377 377 res.setbodygen(meta[b'framegen'])
378 378 return True
379 379 elif action == b'noop':
380 380 return False
381 381 else:
382 382 raise error.ProgrammingError(
383 383 b'unhandled event from reactor: %s' % action
384 384 )
385 385
386 386
387 387 def getdispatchrepo(repo, proto, command):
388 388 viewconfig = repo.ui.config(b'server', b'view')
389 389 return repo.filtered(viewconfig)
390 390
391 391
392 392 def dispatch(repo, proto, command, redirect):
393 393 """Run a wire protocol command.
394 394
395 395 Returns an iterable of objects that will be sent to the client.
396 396 """
397 397 repo = getdispatchrepo(repo, proto, command)
398 398
399 399 entry = COMMANDS[command]
400 400 func = entry.func
401 401 spec = entry.args
402 402
403 403 args = proto.getargs(spec)
404 404
405 405 # There is some duplicate boilerplate code here for calling the command and
406 406 # emitting objects. It is either that or a lot of indented code that looks
407 407 # like a pyramid (since there are a lot of code paths that result in not
408 408 # using the cacher).
409 409 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
410 410
411 411 # Request is not cacheable. Don't bother instantiating a cacher.
412 412 if not entry.cachekeyfn:
413 413 for o in callcommand():
414 414 yield o
415 415 return
416 416
417 417 if redirect:
418 418 redirecttargets = redirect[b'targets']
419 419 redirecthashes = redirect[b'hashes']
420 420 else:
421 421 redirecttargets = []
422 422 redirecthashes = []
423 423
424 424 cacher = makeresponsecacher(
425 425 repo,
426 426 proto,
427 427 command,
428 428 args,
429 429 cborutil.streamencode,
430 430 redirecttargets=redirecttargets,
431 431 redirecthashes=redirecthashes,
432 432 )
433 433
434 434 # But we have no cacher. Do default handling.
435 435 if not cacher:
436 436 for o in callcommand():
437 437 yield o
438 438 return
439 439
440 440 with cacher:
441 441 cachekey = entry.cachekeyfn(
442 442 repo, proto, cacher, **pycompat.strkwargs(args)
443 443 )
444 444
445 445 # No cache key or the cacher doesn't like it. Do default handling.
446 446 if cachekey is None or not cacher.setcachekey(cachekey):
447 447 for o in callcommand():
448 448 yield o
449 449 return
450 450
451 451 # Serve it from the cache, if possible.
452 452 cached = cacher.lookup()
453 453
454 454 if cached:
455 455 for o in cached[b'objs']:
456 456 yield o
457 457 return
458 458
459 459 # Else call the command and feed its output into the cacher, allowing
460 460 # the cacher to buffer/mutate objects as it desires.
461 461 for o in callcommand():
462 462 for o in cacher.onobject(o):
463 463 yield o
464 464
465 465 for o in cacher.onfinished():
466 466 yield o
467 467
468 468
469 469 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
470 470 class httpv2protocolhandler(object):
471 471 def __init__(self, req, ui, args=None):
472 472 self._req = req
473 473 self._ui = ui
474 474 self._args = args
475 475
476 476 @property
477 477 def name(self):
478 478 return HTTP_WIREPROTO_V2
479 479
480 480 def getargs(self, args):
481 481 # First look for args that were passed but aren't registered on this
482 482 # command.
483 483 extra = set(self._args) - set(args)
484 484 if extra:
485 485 raise error.WireprotoCommandError(
486 486 b'unsupported argument to command: %s'
487 487 % b', '.join(sorted(extra))
488 488 )
489 489
490 490 # And look for required arguments that are missing.
491 491 missing = {a for a in args if args[a][b'required']} - set(self._args)
492 492
493 493 if missing:
494 494 raise error.WireprotoCommandError(
495 495 b'missing required arguments: %s' % b', '.join(sorted(missing))
496 496 )
497 497
498 498 # Now derive the arguments to pass to the command, taking into
499 499 # account the arguments specified by the client.
500 500 data = {}
501 501 for k, meta in sorted(args.items()):
502 502 # This argument wasn't passed by the client.
503 503 if k not in self._args:
504 504 data[k] = meta[b'default']()
505 505 continue
506 506
507 507 v = self._args[k]
508 508
509 509 # Sets may be expressed as lists. Silently normalize.
510 510 if meta[b'type'] == b'set' and isinstance(v, list):
511 511 v = set(v)
512 512
513 513 # TODO consider more/stronger type validation.
514 514
515 515 data[k] = v
516 516
517 517 return data
518 518
519 519 def getprotocaps(self):
520 520 # Protocol capabilities are currently not implemented for HTTP V2.
521 521 return set()
522 522
523 523 def getpayload(self):
524 524 raise NotImplementedError
525 525
526 526 @contextlib.contextmanager
527 527 def mayberedirectstdio(self):
528 528 raise NotImplementedError
529 529
530 530 def client(self):
531 531 raise NotImplementedError
532 532
533 533 def addcapabilities(self, repo, caps):
534 534 return caps
535 535
536 536 def checkperm(self, perm):
537 537 raise NotImplementedError
538 538
539 539
540 540 def httpv2apidescriptor(req, repo):
541 541 proto = httpv2protocolhandler(req, repo.ui)
542 542
543 543 return _capabilitiesv2(repo, proto)
544 544
545 545
546 546 def _capabilitiesv2(repo, proto):
547 547 """Obtain the set of capabilities for version 2 transports.
548 548
549 549 These capabilities are distinct from the capabilities for version 1
550 550 transports.
551 551 """
552 552 caps = {
553 553 b'commands': {},
554 554 b'framingmediatypes': [FRAMINGTYPE],
555 555 b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
556 556 }
557 557
558 558 for command, entry in COMMANDS.items():
559 559 args = {}
560 560
561 561 for arg, meta in entry.args.items():
562 562 args[arg] = {
563 563 # TODO should this be a normalized type using CBOR's
564 564 # terminology?
565 565 b'type': meta[b'type'],
566 566 b'required': meta[b'required'],
567 567 }
568 568
569 569 if not meta[b'required']:
570 570 args[arg][b'default'] = meta[b'default']()
571 571
572 572 if meta[b'validvalues']:
573 573 args[arg][b'validvalues'] = meta[b'validvalues']
574 574
575 575 # TODO this type of check should be defined in a per-command callback.
576 576 if (
577 577 command == b'rawstorefiledata'
578 578 and not streamclone.allowservergeneration(repo)
579 579 ):
580 580 continue
581 581
582 # pytype: disable=unsupported-operands
582 583 caps[b'commands'][command] = {
583 584 b'args': args,
584 585 b'permissions': [entry.permission],
585 586 }
587 # pytype: enable=unsupported-operands
586 588
587 589 if entry.extracapabilitiesfn:
588 590 extracaps = entry.extracapabilitiesfn(repo, proto)
589 591 caps[b'commands'][command].update(extracaps)
590 592
591 593 caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
592 594
593 595 targets = getadvertisedredirecttargets(repo, proto)
594 596 if targets:
595 597 caps[b'redirect'] = {
596 598 b'targets': [],
597 599 b'hashes': [b'sha256', b'sha1'],
598 600 }
599 601
600 602 for target in targets:
601 603 entry = {
602 604 b'name': target[b'name'],
603 605 b'protocol': target[b'protocol'],
604 606 b'uris': target[b'uris'],
605 607 }
606 608
607 609 for key in (b'snirequired', b'tlsversions'):
608 610 if key in target:
609 611 entry[key] = target[key]
610 612
613 # pytype: disable=attribute-error
611 614 caps[b'redirect'][b'targets'].append(entry)
615 # pytype: enable=attribute-error
612 616
613 617 return proto.addcapabilities(repo, caps)
614 618
615 619
616 620 def getadvertisedredirecttargets(repo, proto):
617 621 """Obtain a list of content redirect targets.
618 622
619 623 Returns a list containing potential redirect targets that will be
620 624 advertised in capabilities data. Each dict MUST have the following
621 625 keys:
622 626
623 627 name
624 628 The name of this redirect target. This is the identifier clients use
625 629 to refer to a target. It is transferred as part of every command
626 630 request.
627 631
628 632 protocol
629 633 Network protocol used by this target. Typically this is the string
630 634 in front of the ``://`` in a URL. e.g. ``https``.
631 635
632 636 uris
633 637 List of representative URIs for this target. Clients can use the
634 638 URIs to test parsing for compatibility or for ordering preference
635 639 for which target to use.
636 640
637 641 The following optional keys are recognized:
638 642
639 643 snirequired
640 644 Bool indicating if Server Name Indication (SNI) is required to
641 645 connect to this target.
642 646
643 647 tlsversions
644 648 List of bytes indicating which TLS versions are supported by this
645 649 target.
646 650
647 651 By default, clients reflect the target order advertised by servers
648 652 and servers will use the first client-advertised target when picking
649 653 a redirect target. So targets should be advertised in the order the
650 654 server prefers they be used.
651 655 """
652 656 return []
653 657
654 658
655 659 def wireprotocommand(
656 660 name,
657 661 args=None,
658 662 permission=b'push',
659 663 cachekeyfn=None,
660 664 extracapabilitiesfn=None,
661 665 ):
662 666 """Decorator to declare a wire protocol command.
663 667
664 668 ``name`` is the name of the wire protocol command being provided.
665 669
666 670 ``args`` is a dict defining arguments accepted by the command. Keys are
667 671 the argument name. Values are dicts with the following keys:
668 672
669 673 ``type``
670 674 The argument data type. Must be one of the following string
671 675 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
672 676 or ``bool``.
673 677
674 678 ``default``
675 679 A callable returning the default value for this argument. If not
676 680 specified, ``None`` will be the default value.
677 681
678 682 ``example``
679 683 An example value for this argument.
680 684
681 685 ``validvalues``
682 686 Set of recognized values for this argument.
683 687
684 688 ``permission`` defines the permission type needed to run this command.
685 689 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
686 690 respectively. Default is to assume command requires ``push`` permissions
687 691 because otherwise commands not declaring their permissions could modify
688 692 a repository that is supposed to be read-only.
689 693
690 694 ``cachekeyfn`` defines an optional callable that can derive the
691 695 cache key for this request.
692 696
693 697 ``extracapabilitiesfn`` defines an optional callable that defines extra
694 698 command capabilities/parameters that are advertised next to the command
695 699 in the capabilities data structure describing the server. The callable
696 700 receives as arguments the repository and protocol objects. It returns
697 701 a dict of extra fields to add to the command descriptor.
698 702
699 703 Wire protocol commands are generators of objects to be serialized and
700 704 sent to the client.
701 705
702 706 If a command raises an uncaught exception, this will be translated into
703 707 a command error.
704 708
705 709 All commands can opt in to being cacheable by defining a function
706 710 (``cachekeyfn``) that is called to derive a cache key. This function
707 711 receives the same arguments as the command itself plus a ``cacher``
708 712 argument containing the active cacher for the request and returns a bytes
709 713 containing the key in a cache the response to this command may be cached
710 714 under.
711 715 """
712 716 transports = {
713 717 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2
714 718 }
715 719
716 720 if permission not in (b'push', b'pull'):
717 721 raise error.ProgrammingError(
718 722 b'invalid wire protocol permission; '
719 723 b'got %s; expected "push" or "pull"' % permission
720 724 )
721 725
722 726 if args is None:
723 727 args = {}
724 728
725 729 if not isinstance(args, dict):
726 730 raise error.ProgrammingError(
727 731 b'arguments for version 2 commands must be declared as dicts'
728 732 )
729 733
730 734 for arg, meta in args.items():
731 735 if arg == b'*':
732 736 raise error.ProgrammingError(
733 737 b'* argument name not allowed on version 2 commands'
734 738 )
735 739
736 740 if not isinstance(meta, dict):
737 741 raise error.ProgrammingError(
738 742 b'arguments for version 2 commands '
739 743 b'must declare metadata as a dict'
740 744 )
741 745
742 746 if b'type' not in meta:
743 747 raise error.ProgrammingError(
744 748 b'%s argument for command %s does not '
745 749 b'declare type field' % (arg, name)
746 750 )
747 751
748 752 if meta[b'type'] not in (
749 753 b'bytes',
750 754 b'int',
751 755 b'list',
752 756 b'dict',
753 757 b'set',
754 758 b'bool',
755 759 ):
756 760 raise error.ProgrammingError(
757 761 b'%s argument for command %s has '
758 762 b'illegal type: %s' % (arg, name, meta[b'type'])
759 763 )
760 764
761 765 if b'example' not in meta:
762 766 raise error.ProgrammingError(
763 767 b'%s argument for command %s does not '
764 768 b'declare example field' % (arg, name)
765 769 )
766 770
767 771 meta[b'required'] = b'default' not in meta
768 772
769 773 meta.setdefault(b'default', lambda: None)
770 774 meta.setdefault(b'validvalues', None)
771 775
772 776 def register(func):
773 777 if name in COMMANDS:
774 778 raise error.ProgrammingError(
775 779 b'%s command already registered for version 2' % name
776 780 )
777 781
778 782 COMMANDS[name] = wireprototypes.commandentry(
779 783 func,
780 784 args=args,
781 785 transports=transports,
782 786 permission=permission,
783 787 cachekeyfn=cachekeyfn,
784 788 extracapabilitiesfn=extracapabilitiesfn,
785 789 )
786 790
787 791 return func
788 792
789 793 return register
790 794
791 795
792 796 def makecommandcachekeyfn(command, localversion=None, allargs=False):
793 797 """Construct a cache key derivation function with common features.
794 798
795 799 By default, the cache key is a hash of:
796 800
797 801 * The command name.
798 802 * A global cache version number.
799 803 * A local cache version number (passed via ``localversion``).
800 804 * All the arguments passed to the command.
801 805 * The media type used.
802 806 * Wire protocol version string.
803 807 * The repository path.
804 808 """
805 809 if not allargs:
806 810 raise error.ProgrammingError(
807 811 b'only allargs=True is currently supported'
808 812 )
809 813
810 814 if localversion is None:
811 815 raise error.ProgrammingError(b'must set localversion argument value')
812 816
813 817 def cachekeyfn(repo, proto, cacher, **args):
814 818 spec = COMMANDS[command]
815 819
816 820 # Commands that mutate the repo can not be cached.
817 821 if spec.permission == b'push':
818 822 return None
819 823
820 824 # TODO config option to disable caching.
821 825
822 826 # Our key derivation strategy is to construct a data structure
823 827 # holding everything that could influence cacheability and to hash
824 828 # the CBOR representation of that. Using CBOR seems like it might
825 829 # be overkill. However, simpler hashing mechanisms are prone to
826 830 # duplicate input issues. e.g. if you just concatenate two values,
827 831 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
828 832 # "padding" between values and prevents these problems.
829 833
830 834 # Seed the hash with various data.
831 835 state = {
832 836 # To invalidate all cache keys.
833 837 b'globalversion': GLOBAL_CACHE_VERSION,
834 838 # More granular cache key invalidation.
835 839 b'localversion': localversion,
836 840 # Cache keys are segmented by command.
837 841 b'command': command,
838 842 # Throw in the media type and API version strings so changes
839 843 # to exchange semantics invalid cache.
840 844 b'mediatype': FRAMINGTYPE,
841 845 b'version': HTTP_WIREPROTO_V2,
842 846 # So same requests for different repos don't share cache keys.
843 847 b'repo': repo.root,
844 848 }
845 849
846 850 # The arguments passed to us will have already been normalized.
847 851 # Default values will be set, etc. This is important because it
848 852 # means that it doesn't matter if clients send an explicit argument
849 853 # or rely on the default value: it will all normalize to the same
850 854 # set of arguments on the server and therefore the same cache key.
851 855 #
852 856 # Arguments by their very nature must support being encoded to CBOR.
853 857 # And the CBOR encoder is deterministic. So we hash the arguments
854 858 # by feeding the CBOR of their representation into the hasher.
855 859 if allargs:
856 860 state[b'args'] = pycompat.byteskwargs(args)
857 861
858 862 cacher.adjustcachekeystate(state)
859 863
860 864 hasher = hashutil.sha1()
861 865 for chunk in cborutil.streamencode(state):
862 866 hasher.update(chunk)
863 867
864 868 return pycompat.sysbytes(hasher.hexdigest())
865 869
866 870 return cachekeyfn
867 871
868 872
869 873 def makeresponsecacher(
870 874 repo, proto, command, args, objencoderfn, redirecttargets, redirecthashes
871 875 ):
872 876 """Construct a cacher for a cacheable command.
873 877
874 878 Returns an ``iwireprotocolcommandcacher`` instance.
875 879
876 880 Extensions can monkeypatch this function to provide custom caching
877 881 backends.
878 882 """
879 883 return None
880 884
881 885
882 886 def resolvenodes(repo, revisions):
883 887 """Resolve nodes from a revisions specifier data structure."""
884 888 cl = repo.changelog
885 889 clhasnode = cl.hasnode
886 890
887 891 seen = set()
888 892 nodes = []
889 893
890 894 if not isinstance(revisions, list):
891 895 raise error.WireprotoCommandError(
892 896 b'revisions must be defined as an array'
893 897 )
894 898
895 899 for spec in revisions:
896 900 if b'type' not in spec:
897 901 raise error.WireprotoCommandError(
898 902 b'type key not present in revision specifier'
899 903 )
900 904
901 905 typ = spec[b'type']
902 906
903 907 if typ == b'changesetexplicit':
904 908 if b'nodes' not in spec:
905 909 raise error.WireprotoCommandError(
906 910 b'nodes key not present in changesetexplicit revision '
907 911 b'specifier'
908 912 )
909 913
910 914 for node in spec[b'nodes']:
911 915 if node not in seen:
912 916 nodes.append(node)
913 917 seen.add(node)
914 918
915 919 elif typ == b'changesetexplicitdepth':
916 920 for key in (b'nodes', b'depth'):
917 921 if key not in spec:
918 922 raise error.WireprotoCommandError(
919 923 b'%s key not present in changesetexplicitdepth revision '
920 924 b'specifier',
921 925 (key,),
922 926 )
923 927
924 928 for rev in repo.revs(
925 929 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
926 930 ):
927 931 node = cl.node(rev)
928 932
929 933 if node not in seen:
930 934 nodes.append(node)
931 935 seen.add(node)
932 936
933 937 elif typ == b'changesetdagrange':
934 938 for key in (b'roots', b'heads'):
935 939 if key not in spec:
936 940 raise error.WireprotoCommandError(
937 941 b'%s key not present in changesetdagrange revision '
938 942 b'specifier',
939 943 (key,),
940 944 )
941 945
942 946 if not spec[b'heads']:
943 947 raise error.WireprotoCommandError(
944 948 b'heads key in changesetdagrange cannot be empty'
945 949 )
946 950
947 951 if spec[b'roots']:
948 952 common = [n for n in spec[b'roots'] if clhasnode(n)]
949 953 else:
950 954 common = [repo.nullid]
951 955
952 956 for n in discovery.outgoing(repo, common, spec[b'heads']).missing:
953 957 if n not in seen:
954 958 nodes.append(n)
955 959 seen.add(n)
956 960
957 961 else:
958 962 raise error.WireprotoCommandError(
959 963 b'unknown revision specifier type: %s', (typ,)
960 964 )
961 965
962 966 return nodes
963 967
964 968
965 969 @wireprotocommand(b'branchmap', permission=b'pull')
966 970 def branchmapv2(repo, proto):
967 971 yield {
968 972 encoding.fromlocal(k): v
969 973 for k, v in pycompat.iteritems(repo.branchmap())
970 974 }
971 975
972 976
973 977 @wireprotocommand(b'capabilities', permission=b'pull')
974 978 def capabilitiesv2(repo, proto):
975 979 yield _capabilitiesv2(repo, proto)
976 980
977 981
978 982 @wireprotocommand(
979 983 b'changesetdata',
980 984 args={
981 985 b'revisions': {
982 986 b'type': b'list',
983 987 b'example': [
984 988 {
985 989 b'type': b'changesetexplicit',
986 990 b'nodes': [b'abcdef...'],
987 991 }
988 992 ],
989 993 },
990 994 b'fields': {
991 995 b'type': b'set',
992 996 b'default': set,
993 997 b'example': {b'parents', b'revision'},
994 998 b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
995 999 },
996 1000 },
997 1001 permission=b'pull',
998 1002 )
999 1003 def changesetdata(repo, proto, revisions, fields):
1000 1004 # TODO look for unknown fields and abort when they can't be serviced.
1001 1005 # This could probably be validated by dispatcher using validvalues.
1002 1006
1003 1007 cl = repo.changelog
1004 1008 outgoing = resolvenodes(repo, revisions)
1005 1009 publishing = repo.publishing()
1006 1010
1007 1011 if outgoing:
1008 1012 repo.hook(b'preoutgoing', throw=True, source=b'serve')
1009 1013
1010 1014 yield {
1011 1015 b'totalitems': len(outgoing),
1012 1016 }
1013 1017
1014 1018 # The phases of nodes already transferred to the client may have changed
1015 1019 # since the client last requested data. We send phase-only records
1016 1020 # for these revisions, if requested.
1017 1021 # TODO actually do this. We'll probably want to emit phase heads
1018 1022 # in the ancestry set of the outgoing revisions. This will ensure
1019 1023 # that phase updates within that set are seen.
1020 1024 if b'phase' in fields:
1021 1025 pass
1022 1026
1023 1027 nodebookmarks = {}
1024 1028 for mark, node in repo._bookmarks.items():
1025 1029 nodebookmarks.setdefault(node, set()).add(mark)
1026 1030
1027 1031 # It is already topologically sorted by revision number.
1028 1032 for node in outgoing:
1029 1033 d = {
1030 1034 b'node': node,
1031 1035 }
1032 1036
1033 1037 if b'parents' in fields:
1034 1038 d[b'parents'] = cl.parents(node)
1035 1039
1036 1040 if b'phase' in fields:
1037 1041 if publishing:
1038 1042 d[b'phase'] = b'public'
1039 1043 else:
1040 1044 ctx = repo[node]
1041 1045 d[b'phase'] = ctx.phasestr()
1042 1046
1043 1047 if b'bookmarks' in fields and node in nodebookmarks:
1044 1048 d[b'bookmarks'] = sorted(nodebookmarks[node])
1045 1049 del nodebookmarks[node]
1046 1050
1047 1051 followingmeta = []
1048 1052 followingdata = []
1049 1053
1050 1054 if b'revision' in fields:
1051 1055 revisiondata = cl.revision(node)
1052 1056 followingmeta.append((b'revision', len(revisiondata)))
1053 1057 followingdata.append(revisiondata)
1054 1058
1055 1059 # TODO make it possible for extensions to wrap a function or register
1056 1060 # a handler to service custom fields.
1057 1061
1058 1062 if followingmeta:
1059 1063 d[b'fieldsfollowing'] = followingmeta
1060 1064
1061 1065 yield d
1062 1066
1063 1067 for extra in followingdata:
1064 1068 yield extra
1065 1069
1066 1070 # If requested, send bookmarks from nodes that didn't have revision
1067 1071 # data sent so receiver is aware of any bookmark updates.
1068 1072 if b'bookmarks' in fields:
1069 1073 for node, marks in sorted(pycompat.iteritems(nodebookmarks)):
1070 1074 yield {
1071 1075 b'node': node,
1072 1076 b'bookmarks': sorted(marks),
1073 1077 }
1074 1078
1075 1079
1076 1080 class FileAccessError(Exception):
1077 1081 """Represents an error accessing a specific file."""
1078 1082
1079 1083 def __init__(self, path, msg, args):
1080 1084 self.path = path
1081 1085 self.msg = msg
1082 1086 self.args = args
1083 1087
1084 1088
1085 1089 def getfilestore(repo, proto, path):
1086 1090 """Obtain a file storage object for use with wire protocol.
1087 1091
1088 1092 Exists as a standalone function so extensions can monkeypatch to add
1089 1093 access control.
1090 1094 """
1091 1095 # This seems to work even if the file doesn't exist. So catch
1092 1096 # "empty" files and return an error.
1093 1097 fl = repo.file(path)
1094 1098
1095 1099 if not len(fl):
1096 1100 raise FileAccessError(path, b'unknown file: %s', (path,))
1097 1101
1098 1102 return fl
1099 1103
1100 1104
1101 1105 def emitfilerevisions(repo, path, revisions, linknodes, fields):
1102 1106 for revision in revisions:
1103 1107 d = {
1104 1108 b'node': revision.node,
1105 1109 }
1106 1110
1107 1111 if b'parents' in fields:
1108 1112 d[b'parents'] = [revision.p1node, revision.p2node]
1109 1113
1110 1114 if b'linknode' in fields:
1111 1115 d[b'linknode'] = linknodes[revision.node]
1112 1116
1113 1117 followingmeta = []
1114 1118 followingdata = []
1115 1119
1116 1120 if b'revision' in fields:
1117 1121 if revision.revision is not None:
1118 1122 followingmeta.append((b'revision', len(revision.revision)))
1119 1123 followingdata.append(revision.revision)
1120 1124 else:
1121 1125 d[b'deltabasenode'] = revision.basenode
1122 1126 followingmeta.append((b'delta', len(revision.delta)))
1123 1127 followingdata.append(revision.delta)
1124 1128
1125 1129 if followingmeta:
1126 1130 d[b'fieldsfollowing'] = followingmeta
1127 1131
1128 1132 yield d
1129 1133
1130 1134 for extra in followingdata:
1131 1135 yield extra
1132 1136
1133 1137
1134 1138 def makefilematcher(repo, pathfilter):
1135 1139 """Construct a matcher from a path filter dict."""
1136 1140
1137 1141 # Validate values.
1138 1142 if pathfilter:
1139 1143 for key in (b'include', b'exclude'):
1140 1144 for pattern in pathfilter.get(key, []):
1141 1145 if not pattern.startswith((b'path:', b'rootfilesin:')):
1142 1146 raise error.WireprotoCommandError(
1143 1147 b'%s pattern must begin with `path:` or `rootfilesin:`; '
1144 1148 b'got %s',
1145 1149 (key, pattern),
1146 1150 )
1147 1151
1148 1152 if pathfilter:
1149 1153 matcher = matchmod.match(
1150 1154 repo.root,
1151 1155 b'',
1152 1156 include=pathfilter.get(b'include', []),
1153 1157 exclude=pathfilter.get(b'exclude', []),
1154 1158 )
1155 1159 else:
1156 1160 matcher = matchmod.match(repo.root, b'')
1157 1161
1158 1162 # Requested patterns could include files not in the local store. So
1159 1163 # filter those out.
1160 1164 return repo.narrowmatch(matcher)
1161 1165
1162 1166
1163 1167 @wireprotocommand(
1164 1168 b'filedata',
1165 1169 args={
1166 1170 b'haveparents': {
1167 1171 b'type': b'bool',
1168 1172 b'default': lambda: False,
1169 1173 b'example': True,
1170 1174 },
1171 1175 b'nodes': {
1172 1176 b'type': b'list',
1173 1177 b'example': [b'0123456...'],
1174 1178 },
1175 1179 b'fields': {
1176 1180 b'type': b'set',
1177 1181 b'default': set,
1178 1182 b'example': {b'parents', b'revision'},
1179 1183 b'validvalues': {b'parents', b'revision', b'linknode'},
1180 1184 },
1181 1185 b'path': {
1182 1186 b'type': b'bytes',
1183 1187 b'example': b'foo.txt',
1184 1188 },
1185 1189 },
1186 1190 permission=b'pull',
1187 1191 # TODO censoring a file revision won't invalidate the cache.
1188 1192 # Figure out a way to take censoring into account when deriving
1189 1193 # the cache key.
1190 1194 cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True),
1191 1195 )
1192 1196 def filedata(repo, proto, haveparents, nodes, fields, path):
1193 1197 # TODO this API allows access to file revisions that are attached to
1194 1198 # secret changesets. filesdata does not have this problem. Maybe this
1195 1199 # API should be deleted?
1196 1200
1197 1201 try:
1198 1202 # Extensions may wish to access the protocol handler.
1199 1203 store = getfilestore(repo, proto, path)
1200 1204 except FileAccessError as e:
1201 1205 raise error.WireprotoCommandError(e.msg, e.args)
1202 1206
1203 1207 clnode = repo.changelog.node
1204 1208 linknodes = {}
1205 1209
1206 1210 # Validate requested nodes.
1207 1211 for node in nodes:
1208 1212 try:
1209 1213 store.rev(node)
1210 1214 except error.LookupError:
1211 1215 raise error.WireprotoCommandError(
1212 1216 b'unknown file node: %s', (hex(node),)
1213 1217 )
1214 1218
1215 1219 # TODO by creating the filectx against a specific file revision
1216 1220 # instead of changeset, linkrev() is always used. This is wrong for
1217 1221 # cases where linkrev() may refer to a hidden changeset. But since this
1218 1222 # API doesn't know anything about changesets, we're not sure how to
1219 1223 # disambiguate the linknode. Perhaps we should delete this API?
1220 1224 fctx = repo.filectx(path, fileid=node)
1221 1225 linknodes[node] = clnode(fctx.introrev())
1222 1226
1223 1227 revisions = store.emitrevisions(
1224 1228 nodes,
1225 1229 revisiondata=b'revision' in fields,
1226 1230 assumehaveparentrevisions=haveparents,
1227 1231 )
1228 1232
1229 1233 yield {
1230 1234 b'totalitems': len(nodes),
1231 1235 }
1232 1236
1233 1237 for o in emitfilerevisions(repo, path, revisions, linknodes, fields):
1234 1238 yield o
1235 1239
1236 1240
1237 1241 def filesdatacapabilities(repo, proto):
1238 1242 batchsize = repo.ui.configint(
1239 1243 b'experimental', b'server.filesdata.recommended-batch-size'
1240 1244 )
1241 1245 return {
1242 1246 b'recommendedbatchsize': batchsize,
1243 1247 }
1244 1248
1245 1249
1246 1250 @wireprotocommand(
1247 1251 b'filesdata',
1248 1252 args={
1249 1253 b'haveparents': {
1250 1254 b'type': b'bool',
1251 1255 b'default': lambda: False,
1252 1256 b'example': True,
1253 1257 },
1254 1258 b'fields': {
1255 1259 b'type': b'set',
1256 1260 b'default': set,
1257 1261 b'example': {b'parents', b'revision'},
1258 1262 b'validvalues': {
1259 1263 b'firstchangeset',
1260 1264 b'linknode',
1261 1265 b'parents',
1262 1266 b'revision',
1263 1267 },
1264 1268 },
1265 1269 b'pathfilter': {
1266 1270 b'type': b'dict',
1267 1271 b'default': lambda: None,
1268 1272 b'example': {b'include': [b'path:tests']},
1269 1273 },
1270 1274 b'revisions': {
1271 1275 b'type': b'list',
1272 1276 b'example': [
1273 1277 {
1274 1278 b'type': b'changesetexplicit',
1275 1279 b'nodes': [b'abcdef...'],
1276 1280 }
1277 1281 ],
1278 1282 },
1279 1283 },
1280 1284 permission=b'pull',
1281 1285 # TODO censoring a file revision won't invalidate the cache.
1282 1286 # Figure out a way to take censoring into account when deriving
1283 1287 # the cache key.
1284 1288 cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True),
1285 1289 extracapabilitiesfn=filesdatacapabilities,
1286 1290 )
1287 1291 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
1288 1292 # TODO This should operate on a repo that exposes obsolete changesets. There
1289 1293 # is a race between a client making a push that obsoletes a changeset and
1290 1294 # another client fetching files data for that changeset. If a client has a
1291 1295 # changeset, it should probably be allowed to access files data for that
1292 1296 # changeset.
1293 1297
1294 1298 outgoing = resolvenodes(repo, revisions)
1295 1299 filematcher = makefilematcher(repo, pathfilter)
1296 1300
1297 1301 # path -> {fnode: linknode}
1298 1302 fnodes = collections.defaultdict(dict)
1299 1303
1300 1304 # We collect the set of relevant file revisions by iterating the changeset
1301 1305 # revisions and either walking the set of files recorded in the changeset
1302 1306 # or by walking the manifest at that revision. There is probably room for a
1303 1307 # storage-level API to request this data, as it can be expensive to compute
1304 1308 # and would benefit from caching or alternate storage from what revlogs
1305 1309 # provide.
1306 1310 for node in outgoing:
1307 1311 ctx = repo[node]
1308 1312 mctx = ctx.manifestctx()
1309 1313 md = mctx.read()
1310 1314
1311 1315 if haveparents:
1312 1316 checkpaths = ctx.files()
1313 1317 else:
1314 1318 checkpaths = md.keys()
1315 1319
1316 1320 for path in checkpaths:
1317 1321 fnode = md[path]
1318 1322
1319 1323 if path in fnodes and fnode in fnodes[path]:
1320 1324 continue
1321 1325
1322 1326 if not filematcher(path):
1323 1327 continue
1324 1328
1325 1329 fnodes[path].setdefault(fnode, node)
1326 1330
1327 1331 yield {
1328 1332 b'totalpaths': len(fnodes),
1329 1333 b'totalitems': sum(len(v) for v in fnodes.values()),
1330 1334 }
1331 1335
1332 1336 for path, filenodes in sorted(fnodes.items()):
1333 1337 try:
1334 1338 store = getfilestore(repo, proto, path)
1335 1339 except FileAccessError as e:
1336 1340 raise error.WireprotoCommandError(e.msg, e.args)
1337 1341
1338 1342 yield {
1339 1343 b'path': path,
1340 1344 b'totalitems': len(filenodes),
1341 1345 }
1342 1346
1343 1347 revisions = store.emitrevisions(
1344 1348 filenodes.keys(),
1345 1349 revisiondata=b'revision' in fields,
1346 1350 assumehaveparentrevisions=haveparents,
1347 1351 )
1348 1352
1349 1353 for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
1350 1354 yield o
1351 1355
1352 1356
1353 1357 @wireprotocommand(
1354 1358 b'heads',
1355 1359 args={
1356 1360 b'publiconly': {
1357 1361 b'type': b'bool',
1358 1362 b'default': lambda: False,
1359 1363 b'example': False,
1360 1364 },
1361 1365 },
1362 1366 permission=b'pull',
1363 1367 )
1364 1368 def headsv2(repo, proto, publiconly):
1365 1369 if publiconly:
1366 1370 repo = repo.filtered(b'immutable')
1367 1371
1368 1372 yield repo.heads()
1369 1373
1370 1374
1371 1375 @wireprotocommand(
1372 1376 b'known',
1373 1377 args={
1374 1378 b'nodes': {
1375 1379 b'type': b'list',
1376 1380 b'default': list,
1377 1381 b'example': [b'deadbeef'],
1378 1382 },
1379 1383 },
1380 1384 permission=b'pull',
1381 1385 )
1382 1386 def knownv2(repo, proto, nodes):
1383 1387 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1384 1388 yield result
1385 1389
1386 1390
1387 1391 @wireprotocommand(
1388 1392 b'listkeys',
1389 1393 args={
1390 1394 b'namespace': {
1391 1395 b'type': b'bytes',
1392 1396 b'example': b'ns',
1393 1397 },
1394 1398 },
1395 1399 permission=b'pull',
1396 1400 )
1397 1401 def listkeysv2(repo, proto, namespace):
1398 1402 keys = repo.listkeys(encoding.tolocal(namespace))
1399 1403 keys = {
1400 1404 encoding.fromlocal(k): encoding.fromlocal(v)
1401 1405 for k, v in pycompat.iteritems(keys)
1402 1406 }
1403 1407
1404 1408 yield keys
1405 1409
1406 1410
1407 1411 @wireprotocommand(
1408 1412 b'lookup',
1409 1413 args={
1410 1414 b'key': {
1411 1415 b'type': b'bytes',
1412 1416 b'example': b'foo',
1413 1417 },
1414 1418 },
1415 1419 permission=b'pull',
1416 1420 )
1417 1421 def lookupv2(repo, proto, key):
1418 1422 key = encoding.tolocal(key)
1419 1423
1420 1424 # TODO handle exception.
1421 1425 node = repo.lookup(key)
1422 1426
1423 1427 yield node
1424 1428
1425 1429
1426 1430 def manifestdatacapabilities(repo, proto):
1427 1431 batchsize = repo.ui.configint(
1428 1432 b'experimental', b'server.manifestdata.recommended-batch-size'
1429 1433 )
1430 1434
1431 1435 return {
1432 1436 b'recommendedbatchsize': batchsize,
1433 1437 }
1434 1438
1435 1439
1436 1440 @wireprotocommand(
1437 1441 b'manifestdata',
1438 1442 args={
1439 1443 b'nodes': {
1440 1444 b'type': b'list',
1441 1445 b'example': [b'0123456...'],
1442 1446 },
1443 1447 b'haveparents': {
1444 1448 b'type': b'bool',
1445 1449 b'default': lambda: False,
1446 1450 b'example': True,
1447 1451 },
1448 1452 b'fields': {
1449 1453 b'type': b'set',
1450 1454 b'default': set,
1451 1455 b'example': {b'parents', b'revision'},
1452 1456 b'validvalues': {b'parents', b'revision'},
1453 1457 },
1454 1458 b'tree': {
1455 1459 b'type': b'bytes',
1456 1460 b'example': b'',
1457 1461 },
1458 1462 },
1459 1463 permission=b'pull',
1460 1464 cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True),
1461 1465 extracapabilitiesfn=manifestdatacapabilities,
1462 1466 )
1463 1467 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1464 1468 store = repo.manifestlog.getstorage(tree)
1465 1469
1466 1470 # Validate the node is known and abort on unknown revisions.
1467 1471 for node in nodes:
1468 1472 try:
1469 1473 store.rev(node)
1470 1474 except error.LookupError:
1471 1475 raise error.WireprotoCommandError(b'unknown node: %s', (node,))
1472 1476
1473 1477 revisions = store.emitrevisions(
1474 1478 nodes,
1475 1479 revisiondata=b'revision' in fields,
1476 1480 assumehaveparentrevisions=haveparents,
1477 1481 )
1478 1482
1479 1483 yield {
1480 1484 b'totalitems': len(nodes),
1481 1485 }
1482 1486
1483 1487 for revision in revisions:
1484 1488 d = {
1485 1489 b'node': revision.node,
1486 1490 }
1487 1491
1488 1492 if b'parents' in fields:
1489 1493 d[b'parents'] = [revision.p1node, revision.p2node]
1490 1494
1491 1495 followingmeta = []
1492 1496 followingdata = []
1493 1497
1494 1498 if b'revision' in fields:
1495 1499 if revision.revision is not None:
1496 1500 followingmeta.append((b'revision', len(revision.revision)))
1497 1501 followingdata.append(revision.revision)
1498 1502 else:
1499 1503 d[b'deltabasenode'] = revision.basenode
1500 1504 followingmeta.append((b'delta', len(revision.delta)))
1501 1505 followingdata.append(revision.delta)
1502 1506
1503 1507 if followingmeta:
1504 1508 d[b'fieldsfollowing'] = followingmeta
1505 1509
1506 1510 yield d
1507 1511
1508 1512 for extra in followingdata:
1509 1513 yield extra
1510 1514
1511 1515
1512 1516 @wireprotocommand(
1513 1517 b'pushkey',
1514 1518 args={
1515 1519 b'namespace': {
1516 1520 b'type': b'bytes',
1517 1521 b'example': b'ns',
1518 1522 },
1519 1523 b'key': {
1520 1524 b'type': b'bytes',
1521 1525 b'example': b'key',
1522 1526 },
1523 1527 b'old': {
1524 1528 b'type': b'bytes',
1525 1529 b'example': b'old',
1526 1530 },
1527 1531 b'new': {
1528 1532 b'type': b'bytes',
1529 1533 b'example': b'new',
1530 1534 },
1531 1535 },
1532 1536 permission=b'push',
1533 1537 )
1534 1538 def pushkeyv2(repo, proto, namespace, key, old, new):
1535 1539 # TODO handle ui output redirection
1536 1540 yield repo.pushkey(
1537 1541 encoding.tolocal(namespace),
1538 1542 encoding.tolocal(key),
1539 1543 encoding.tolocal(old),
1540 1544 encoding.tolocal(new),
1541 1545 )
1542 1546
1543 1547
1544 1548 @wireprotocommand(
1545 1549 b'rawstorefiledata',
1546 1550 args={
1547 1551 b'files': {
1548 1552 b'type': b'list',
1549 1553 b'example': [b'changelog', b'manifestlog'],
1550 1554 },
1551 1555 b'pathfilter': {
1552 1556 b'type': b'list',
1553 1557 b'default': lambda: None,
1554 1558 b'example': {b'include': [b'path:tests']},
1555 1559 },
1556 1560 },
1557 1561 permission=b'pull',
1558 1562 )
1559 1563 def rawstorefiledata(repo, proto, files, pathfilter):
1560 1564 if not streamclone.allowservergeneration(repo):
1561 1565 raise error.WireprotoCommandError(b'stream clone is disabled')
1562 1566
1563 1567 # TODO support dynamically advertising what store files "sets" are
1564 1568 # available. For now, we support changelog, manifestlog, and files.
1565 1569 files = set(files)
1566 1570 allowedfiles = {b'changelog', b'manifestlog'}
1567 1571
1568 1572 unsupported = files - allowedfiles
1569 1573 if unsupported:
1570 1574 raise error.WireprotoCommandError(
1571 1575 b'unknown file type: %s', (b', '.join(sorted(unsupported)),)
1572 1576 )
1573 1577
1574 1578 with repo.lock():
1575 1579 topfiles = list(repo.store.topfiles())
1576 1580
1577 1581 sendfiles = []
1578 1582 totalsize = 0
1579 1583
1580 1584 # TODO this is a bunch of storage layer interface abstractions because
1581 1585 # it assumes revlogs.
1582 1586 for rl_type, name, size in topfiles:
1583 1587 # XXX use the `rl_type` for that
1584 1588 if b'changelog' in files and name.startswith(b'00changelog'):
1585 1589 pass
1586 1590 elif b'manifestlog' in files and name.startswith(b'00manifest'):
1587 1591 pass
1588 1592 else:
1589 1593 continue
1590 1594
1591 1595 sendfiles.append((b'store', name, size))
1592 1596 totalsize += size
1593 1597
1594 1598 yield {
1595 1599 b'filecount': len(sendfiles),
1596 1600 b'totalsize': totalsize,
1597 1601 }
1598 1602
1599 1603 for location, name, size in sendfiles:
1600 1604 yield {
1601 1605 b'location': location,
1602 1606 b'path': name,
1603 1607 b'size': size,
1604 1608 }
1605 1609
1606 1610 # We have to use a closure for this to ensure the context manager is
1607 1611 # closed only after sending the final chunk.
1608 1612 def getfiledata():
1609 1613 with repo.svfs(name, b'rb', auditpath=False) as fh:
1610 1614 for chunk in util.filechunkiter(fh, limit=size):
1611 1615 yield chunk
1612 1616
1613 1617 yield wireprototypes.indefinitebytestringresponse(getfiledata())
@@ -1,92 +1,90 b''
1 1 #require pytype py3 slow
2 2
3 3 $ cd $RUNTESTDIR/..
4 4
5 5 Many of the individual files that are excluded here confuse pytype
6 6 because they do a mix of Python 2 and Python 3 things
7 7 conditionally. There's no good way to help it out with that as far as
8 8 I can tell, so let's just hide those files from it for now. We should
9 9 endeavor to empty this list out over time, as some of these are
10 10 probably hiding real problems.
11 11
12 12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
13 13 mercurial/chgserver.py # [attribute-error]
14 14 mercurial/context.py # many [attribute-error]
15 15 mercurial/crecord.py # tons of [attribute-error], [module-attr]
16 16 mercurial/debugcommands.py # [wrong-arg-types]
17 17 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
18 18 mercurial/exchange.py # [attribute-error]
19 19 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
20 20 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
21 21 mercurial/hgweb/webcommands.py # [missing-parameter]
22 22 mercurial/hgweb/wsgicgi.py # confused values in os.environ
23 23 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
24 24 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
25 25 mercurial/keepalive.py # [attribute-error]
26 26 mercurial/localrepo.py # [attribute-error]
27 27 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
28 28 mercurial/minirst.py # [unsupported-operands], [attribute-error]
29 29 mercurial/patch.py # [wrong-arg-types]
30 30 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
31 31 mercurial/pure/parsers.py # [attribute-error]
32 32 mercurial/pycompat.py # bytes vs str issues
33 33 mercurial/repoview.py # [attribute-error]
34 34 mercurial/sslutil.py # [attribute-error]
35 35 mercurial/statprof.py # bytes vs str on TextIO.write() [wrong-arg-types]
36 36 mercurial/testing/storage.py # tons of [attribute-error]
37 37 mercurial/ui.py # [attribute-error], [wrong-arg-types]
38 38 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
39 39 mercurial/util.py # [attribute-error], [wrong-arg-count]
40 40 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
41 41 mercurial/utils/memorytop.py # not 3.6 compatible
42 42 mercurial/win32.py # [not-callable]
43 43 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
44 44 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
45 45 mercurial/wireprotov1peer.py # [attribute-error]
46 46 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
47 mercurial/wireprotov2server.py # [unsupported-operands], [attribute-error]
48 47
49 48 TODO: use --no-cache on test server? Caching the files locally helps during
50 49 development, but may be a hinderance for CI testing.
51 50
52 51 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
53 52 > -x mercurial/bundlerepo.py \
54 53 > -x mercurial/chgserver.py \
55 54 > -x mercurial/context.py \
56 55 > -x mercurial/crecord.py \
57 56 > -x mercurial/debugcommands.py \
58 57 > -x mercurial/dispatch.py \
59 58 > -x mercurial/exchange.py \
60 59 > -x mercurial/hgweb/hgweb_mod.py \
61 60 > -x mercurial/hgweb/server.py \
62 61 > -x mercurial/hgweb/webcommands.py \
63 62 > -x mercurial/hgweb/wsgicgi.py \
64 63 > -x mercurial/httppeer.py \
65 64 > -x mercurial/interfaces \
66 65 > -x mercurial/keepalive.py \
67 66 > -x mercurial/localrepo.py \
68 67 > -x mercurial/manifest.py \
69 68 > -x mercurial/minirst.py \
70 69 > -x mercurial/patch.py \
71 70 > -x mercurial/pure/osutil.py \
72 71 > -x mercurial/pure/parsers.py \
73 72 > -x mercurial/pycompat.py \
74 73 > -x mercurial/repoview.py \
75 74 > -x mercurial/sslutil.py \
76 75 > -x mercurial/statprof.py \
77 76 > -x mercurial/testing/storage.py \
78 77 > -x mercurial/thirdparty \
79 78 > -x mercurial/ui.py \
80 79 > -x mercurial/unionrepo.py \
81 80 > -x mercurial/utils/procutil.py \
82 81 > -x mercurial/utils/memorytop.py \
83 82 > -x mercurial/win32.py \
84 83 > -x mercurial/wireprotoframing.py \
85 84 > -x mercurial/wireprotoserver.py \
86 85 > -x mercurial/wireprotov1peer.py \
87 86 > -x mercurial/wireprotov1server.py \
88 > -x mercurial/wireprotov2server.py \
89 87 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
90 88
91 89 Only show the results on a failure, because the output on success is also
92 90 voluminous and variable.
General Comments 0
You need to be logged in to leave comments. Login now