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