Mercurial > public > mercurial-scm > hg
comparison mercurial/httppeer.py @ 43076:2372284d9457
formatting: blacken the codebase
This is using my patch to black
(https://github.com/psf/black/pull/826) so we don't un-wrap collection
literals.
Done with:
hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S
# skip-blame mass-reformatting only
# no-check-commit reformats foo_bar functions
Differential Revision: https://phab.mercurial-scm.org/D6971
author | Augie Fackler <augie@google.com> |
---|---|
date | Sun, 06 Oct 2019 09:45:02 -0400 |
parents | 37debb6771f5 |
children | 687b865b95ad |
comparison
equal
deleted
inserted
replaced
43075:57875cf423c9 | 43076:2372284d9457 |
---|---|
41 | 41 |
42 httplib = util.httplib | 42 httplib = util.httplib |
43 urlerr = util.urlerr | 43 urlerr = util.urlerr |
44 urlreq = util.urlreq | 44 urlreq = util.urlreq |
45 | 45 |
46 | |
46 def encodevalueinheaders(value, header, limit): | 47 def encodevalueinheaders(value, header, limit): |
47 """Encode a string value into multiple HTTP headers. | 48 """Encode a string value into multiple HTTP headers. |
48 | 49 |
49 ``value`` will be encoded into 1 or more HTTP headers with the names | 50 ``value`` will be encoded into 1 or more HTTP headers with the names |
50 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header | 51 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header |
65 result = [] | 66 result = [] |
66 | 67 |
67 n = 0 | 68 n = 0 |
68 for i in pycompat.xrange(0, len(value), valuelen): | 69 for i in pycompat.xrange(0, len(value), valuelen): |
69 n += 1 | 70 n += 1 |
70 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen]))) | 71 result.append((fmt % str(n), pycompat.strurl(value[i : i + valuelen]))) |
71 | 72 |
72 return result | 73 return result |
74 | |
73 | 75 |
74 class _multifile(object): | 76 class _multifile(object): |
75 def __init__(self, *fileobjs): | 77 def __init__(self, *fileobjs): |
76 for f in fileobjs: | 78 for f in fileobjs: |
77 if not util.safehasattr(f, 'length'): | 79 if not util.safehasattr(f, 'length'): |
78 raise ValueError( | 80 raise ValueError( |
79 '_multifile only supports file objects that ' | 81 '_multifile only supports file objects that ' |
80 'have a length but this one does not:', type(f), f) | 82 'have a length but this one does not:', |
83 type(f), | |
84 f, | |
85 ) | |
81 self._fileobjs = fileobjs | 86 self._fileobjs = fileobjs |
82 self._index = 0 | 87 self._index = 0 |
83 | 88 |
84 @property | 89 @property |
85 def length(self): | 90 def length(self): |
99 | 104 |
100 def seek(self, offset, whence=os.SEEK_SET): | 105 def seek(self, offset, whence=os.SEEK_SET): |
101 if whence != os.SEEK_SET: | 106 if whence != os.SEEK_SET: |
102 raise NotImplementedError( | 107 raise NotImplementedError( |
103 '_multifile does not support anything other' | 108 '_multifile does not support anything other' |
104 ' than os.SEEK_SET for whence on seek()') | 109 ' than os.SEEK_SET for whence on seek()' |
110 ) | |
105 if offset != 0: | 111 if offset != 0: |
106 raise NotImplementedError( | 112 raise NotImplementedError( |
107 '_multifile only supports seeking to start, but that ' | 113 '_multifile only supports seeking to start, but that ' |
108 'could be fixed if you need it') | 114 'could be fixed if you need it' |
115 ) | |
109 for f in self._fileobjs: | 116 for f in self._fileobjs: |
110 f.seek(0) | 117 f.seek(0) |
111 self._index = 0 | 118 self._index = 0 |
112 | 119 |
113 def makev1commandrequest(ui, requestbuilder, caps, capablefn, | 120 |
114 repobaseurl, cmd, args): | 121 def makev1commandrequest( |
122 ui, requestbuilder, caps, capablefn, repobaseurl, cmd, args | |
123 ): | |
115 """Make an HTTP request to run a command for a version 1 client. | 124 """Make an HTTP request to run a command for a version 1 client. |
116 | 125 |
117 ``caps`` is a set of known server capabilities. The value may be | 126 ``caps`` is a set of known server capabilities. The value may be |
118 None if capabilities are not yet known. | 127 None if capabilities are not yet known. |
119 | 128 |
160 | 169 |
161 # Send arguments via HTTP headers. | 170 # Send arguments via HTTP headers. |
162 if headersize > 0: | 171 if headersize > 0: |
163 # The headers can typically carry more data than the URL. | 172 # The headers can typically carry more data than the URL. |
164 encargs = urlreq.urlencode(sorted(args.items())) | 173 encargs = urlreq.urlencode(sorted(args.items())) |
165 for header, value in encodevalueinheaders(encargs, 'X-HgArg', | 174 for header, value in encodevalueinheaders( |
166 headersize): | 175 encargs, 'X-HgArg', headersize |
176 ): | |
167 headers[header] = value | 177 headers[header] = value |
168 # Send arguments via query string (Mercurial <1.9). | 178 # Send arguments via query string (Mercurial <1.9). |
169 else: | 179 else: |
170 q += sorted(args.items()) | 180 q += sorted(args.items()) |
171 | 181 |
200 | 210 |
201 if '0.2tx' in mediatypes and capablefn('compression'): | 211 if '0.2tx' in mediatypes and capablefn('compression'): |
202 # We /could/ compare supported compression formats and prune | 212 # We /could/ compare supported compression formats and prune |
203 # non-mutually supported or error if nothing is mutually supported. | 213 # non-mutually supported or error if nothing is mutually supported. |
204 # For now, send the full list to the server and have it error. | 214 # For now, send the full list to the server and have it error. |
205 comps = [e.wireprotosupport().name for e in | 215 comps = [ |
206 util.compengines.supportedwireengines(util.CLIENTROLE)] | 216 e.wireprotosupport().name |
217 for e in util.compengines.supportedwireengines(util.CLIENTROLE) | |
218 ] | |
207 protoparams.add('comp=%s' % ','.join(comps)) | 219 protoparams.add('comp=%s' % ','.join(comps)) |
208 | 220 |
209 if protoparams: | 221 if protoparams: |
210 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)), | 222 protoheaders = encodevalueinheaders( |
211 'X-HgProto', | 223 ' '.join(sorted(protoparams)), 'X-HgProto', headersize or 1024 |
212 headersize or 1024) | 224 ) |
213 for header, value in protoheaders: | 225 for header, value in protoheaders: |
214 headers[header] = value | 226 headers[header] = value |
215 | 227 |
216 varyheaders = [] | 228 varyheaders = [] |
217 for header in headers: | 229 for header in headers: |
226 if data is not None: | 238 if data is not None: |
227 ui.debug("sending %d bytes\n" % size) | 239 ui.debug("sending %d bytes\n" % size) |
228 req.add_unredirected_header(r'Content-Length', r'%d' % size) | 240 req.add_unredirected_header(r'Content-Length', r'%d' % size) |
229 | 241 |
230 return req, cu, qs | 242 return req, cu, qs |
243 | |
231 | 244 |
232 def _reqdata(req): | 245 def _reqdata(req): |
233 """Get request data, if any. If no data, returns None.""" | 246 """Get request data, if any. If no data, returns None.""" |
234 if pycompat.ispy3: | 247 if pycompat.ispy3: |
235 return req.data | 248 return req.data |
236 if not req.has_data(): | 249 if not req.has_data(): |
237 return None | 250 return None |
238 return req.get_data() | 251 return req.get_data() |
239 | 252 |
253 | |
240 def sendrequest(ui, opener, req): | 254 def sendrequest(ui, opener, req): |
241 """Send a prepared HTTP request. | 255 """Send a prepared HTTP request. |
242 | 256 |
243 Returns the response object. | 257 Returns the response object. |
244 """ | 258 """ |
245 dbg = ui.debug | 259 dbg = ui.debug |
246 if (ui.debugflag | 260 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'): |
247 and ui.configbool('devel', 'debug.peer-request')): | |
248 line = 'devel-peer-request: %s\n' | 261 line = 'devel-peer-request: %s\n' |
249 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()), | 262 dbg( |
250 pycompat.bytesurl(req.get_full_url()))) | 263 line |
264 % '%s %s' | |
265 % ( | |
266 pycompat.bytesurl(req.get_method()), | |
267 pycompat.bytesurl(req.get_full_url()), | |
268 ) | |
269 ) | |
251 hgargssize = None | 270 hgargssize = None |
252 | 271 |
253 for header, value in sorted(req.header_items()): | 272 for header, value in sorted(req.header_items()): |
254 header = pycompat.bytesurl(header) | 273 header = pycompat.bytesurl(header) |
255 value = pycompat.bytesurl(value) | 274 value = pycompat.bytesurl(value) |
259 hgargssize += len(value) | 278 hgargssize += len(value) |
260 else: | 279 else: |
261 dbg(line % ' %s %s' % (header, value)) | 280 dbg(line % ' %s %s' % (header, value)) |
262 | 281 |
263 if hgargssize is not None: | 282 if hgargssize is not None: |
264 dbg(line % ' %d bytes of commands arguments in headers' | 283 dbg( |
265 % hgargssize) | 284 line |
285 % ' %d bytes of commands arguments in headers' | |
286 % hgargssize | |
287 ) | |
266 data = _reqdata(req) | 288 data = _reqdata(req) |
267 if data is not None: | 289 if data is not None: |
268 length = getattr(data, 'length', None) | 290 length = getattr(data, 'length', None) |
269 if length is None: | 291 if length is None: |
270 length = len(data) | 292 length = len(data) |
278 except urlerr.httperror as inst: | 300 except urlerr.httperror as inst: |
279 if inst.code == 401: | 301 if inst.code == 401: |
280 raise error.Abort(_('authorization failed')) | 302 raise error.Abort(_('authorization failed')) |
281 raise | 303 raise |
282 except httplib.HTTPException as inst: | 304 except httplib.HTTPException as inst: |
283 ui.debug('http error requesting %s\n' % | 305 ui.debug( |
284 util.hidepassword(req.get_full_url())) | 306 'http error requesting %s\n' % util.hidepassword(req.get_full_url()) |
307 ) | |
285 ui.traceback() | 308 ui.traceback() |
286 raise IOError(None, inst) | 309 raise IOError(None, inst) |
287 finally: | 310 finally: |
288 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'): | 311 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'): |
289 code = res.code if res else -1 | 312 code = res.code if res else -1 |
290 dbg(line % ' finished in %.4f seconds (%d)' | 313 dbg( |
291 % (util.timer() - start, code)) | 314 line |
315 % ' finished in %.4f seconds (%d)' | |
316 % (util.timer() - start, code) | |
317 ) | |
292 | 318 |
293 # Insert error handlers for common I/O failures. | 319 # Insert error handlers for common I/O failures. |
294 urlmod.wrapresponse(res) | 320 urlmod.wrapresponse(res) |
295 | 321 |
296 return res | 322 return res |
323 | |
297 | 324 |
298 class RedirectedRepoError(error.RepoError): | 325 class RedirectedRepoError(error.RepoError): |
299 def __init__(self, msg, respurl): | 326 def __init__(self, msg, respurl): |
300 super(RedirectedRepoError, self).__init__(msg) | 327 super(RedirectedRepoError, self).__init__(msg) |
301 self.respurl = respurl | 328 self.respurl = respurl |
302 | 329 |
303 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible, | 330 |
304 allowcbor=False): | 331 def parsev1commandresponse( |
332 ui, baseurl, requrl, qs, resp, compressible, allowcbor=False | |
333 ): | |
305 # record the url we got redirected to | 334 # record the url we got redirected to |
306 redirected = False | 335 redirected = False |
307 respurl = pycompat.bytesurl(resp.geturl()) | 336 respurl = pycompat.bytesurl(resp.geturl()) |
308 if respurl.endswith(qs): | 337 if respurl.endswith(qs): |
309 respurl = respurl[:-len(qs)] | 338 respurl = respurl[: -len(qs)] |
310 qsdropped = False | 339 qsdropped = False |
311 else: | 340 else: |
312 qsdropped = True | 341 qsdropped = True |
313 | 342 |
314 if baseurl.rstrip('/') != respurl.rstrip('/'): | 343 if baseurl.rstrip('/') != respurl.rstrip('/'): |
327 | 356 |
328 # Pre 1.0 versions of Mercurial used text/plain and | 357 # Pre 1.0 versions of Mercurial used text/plain and |
329 # application/hg-changegroup. We don't support such old servers. | 358 # application/hg-changegroup. We don't support such old servers. |
330 if not proto.startswith('application/mercurial-'): | 359 if not proto.startswith('application/mercurial-'): |
331 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl)) | 360 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl)) |
332 msg = _("'%s' does not appear to be an hg repository:\n" | 361 msg = _( |
333 "---%%<--- (%s)\n%s\n---%%<---\n") % ( | 362 "'%s' does not appear to be an hg repository:\n" |
334 safeurl, proto or 'no content-type', resp.read(1024)) | 363 "---%%<--- (%s)\n%s\n---%%<---\n" |
364 ) % (safeurl, proto or 'no content-type', resp.read(1024)) | |
335 | 365 |
336 # Some servers may strip the query string from the redirect. We | 366 # Some servers may strip the query string from the redirect. We |
337 # raise a special error type so callers can react to this specially. | 367 # raise a special error type so callers can react to this specially. |
338 if redirected and qsdropped: | 368 if redirected and qsdropped: |
339 raise RedirectedRepoError(msg, respurl) | 369 raise RedirectedRepoError(msg, respurl) |
348 # request during handshake. | 378 # request during handshake. |
349 if subtype == 'cbor': | 379 if subtype == 'cbor': |
350 if allowcbor: | 380 if allowcbor: |
351 return respurl, proto, resp | 381 return respurl, proto, resp |
352 else: | 382 else: |
353 raise error.RepoError(_('unexpected CBOR response from ' | 383 raise error.RepoError( |
354 'server')) | 384 _('unexpected CBOR response from ' 'server') |
385 ) | |
355 | 386 |
356 version_info = tuple([int(n) for n in subtype.split('.')]) | 387 version_info = tuple([int(n) for n in subtype.split('.')]) |
357 except ValueError: | 388 except ValueError: |
358 raise error.RepoError(_("'%s' sent a broken Content-Type " | 389 raise error.RepoError( |
359 "header (%s)") % (safeurl, proto)) | 390 _("'%s' sent a broken Content-Type " "header (%s)") |
391 % (safeurl, proto) | |
392 ) | |
360 | 393 |
361 # TODO consider switching to a decompression reader that uses | 394 # TODO consider switching to a decompression reader that uses |
362 # generators. | 395 # generators. |
363 if version_info == (0, 1): | 396 if version_info == (0, 1): |
364 if compressible: | 397 if compressible: |
371 ename = util.readexactly(resp, elen) | 404 ename = util.readexactly(resp, elen) |
372 engine = util.compengines.forwiretype(ename) | 405 engine = util.compengines.forwiretype(ename) |
373 | 406 |
374 resp = engine.decompressorreader(resp) | 407 resp = engine.decompressorreader(resp) |
375 else: | 408 else: |
376 raise error.RepoError(_("'%s' uses newer protocol %s") % | 409 raise error.RepoError( |
377 (safeurl, subtype)) | 410 _("'%s' uses newer protocol %s") % (safeurl, subtype) |
411 ) | |
378 | 412 |
379 return respurl, proto, resp | 413 return respurl, proto, resp |
414 | |
380 | 415 |
381 class httppeer(wireprotov1peer.wirepeer): | 416 class httppeer(wireprotov1peer.wirepeer): |
382 def __init__(self, ui, path, url, opener, requestbuilder, caps): | 417 def __init__(self, ui, path, url, opener, requestbuilder, caps): |
383 self.ui = ui | 418 self.ui = ui |
384 self._path = path | 419 self._path = path |
407 def canpush(self): | 442 def canpush(self): |
408 return True | 443 return True |
409 | 444 |
410 def close(self): | 445 def close(self): |
411 try: | 446 try: |
412 reqs, sent, recv = (self._urlopener.requestscount, | 447 reqs, sent, recv = ( |
413 self._urlopener.sentbytescount, | 448 self._urlopener.requestscount, |
414 self._urlopener.receivedbytescount) | 449 self._urlopener.sentbytescount, |
450 self._urlopener.receivedbytescount, | |
451 ) | |
415 except AttributeError: | 452 except AttributeError: |
416 return | 453 return |
417 self.ui.note(_('(sent %d HTTP requests and %d bytes; ' | 454 self.ui.note( |
418 'received %d bytes in responses)\n') % | 455 _( |
419 (reqs, sent, recv)) | 456 '(sent %d HTTP requests and %d bytes; ' |
457 'received %d bytes in responses)\n' | |
458 ) | |
459 % (reqs, sent, recv) | |
460 ) | |
420 | 461 |
421 # End of ipeerconnection interface. | 462 # End of ipeerconnection interface. |
422 | 463 |
423 # Begin of ipeercommands interface. | 464 # Begin of ipeercommands interface. |
424 | 465 |
428 # End of ipeercommands interface. | 469 # End of ipeercommands interface. |
429 | 470 |
430 def _callstream(self, cmd, _compressible=False, **args): | 471 def _callstream(self, cmd, _compressible=False, **args): |
431 args = pycompat.byteskwargs(args) | 472 args = pycompat.byteskwargs(args) |
432 | 473 |
433 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder, | 474 req, cu, qs = makev1commandrequest( |
434 self._caps, self.capable, | 475 self.ui, |
435 self._url, cmd, args) | 476 self._requestbuilder, |
477 self._caps, | |
478 self.capable, | |
479 self._url, | |
480 cmd, | |
481 args, | |
482 ) | |
436 | 483 |
437 resp = sendrequest(self.ui, self._urlopener, req) | 484 resp = sendrequest(self.ui, self._urlopener, req) |
438 | 485 |
439 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs, | 486 self._url, ct, resp = parsev1commandresponse( |
440 resp, _compressible) | 487 self.ui, self._url, cu, qs, resp, _compressible |
488 ) | |
441 | 489 |
442 return resp | 490 return resp |
443 | 491 |
444 def _call(self, cmd, **args): | 492 def _call(self, cmd, **args): |
445 fp = self._callstream(cmd, **args) | 493 fp = self._callstream(cmd, **args) |
511 return self._callstream(cmd, _compressible=True, **args) | 559 return self._callstream(cmd, _compressible=True, **args) |
512 | 560 |
513 def _abort(self, exception): | 561 def _abort(self, exception): |
514 raise exception | 562 raise exception |
515 | 563 |
516 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests, | 564 |
517 redirect): | 565 def sendv2request( |
566 ui, opener, requestbuilder, apiurl, permission, requests, redirect | |
567 ): | |
518 wireprotoframing.populatestreamencoders() | 568 wireprotoframing.populatestreamencoders() |
519 | 569 |
520 uiencoders = ui.configlist(b'experimental', b'httppeer.v2-encoder-order') | 570 uiencoders = ui.configlist(b'experimental', b'httppeer.v2-encoder-order') |
521 | 571 |
522 if uiencoders: | 572 if uiencoders: |
523 encoders = [] | 573 encoders = [] |
524 | 574 |
525 for encoder in uiencoders: | 575 for encoder in uiencoders: |
526 if encoder not in wireprotoframing.STREAM_ENCODERS: | 576 if encoder not in wireprotoframing.STREAM_ENCODERS: |
527 ui.warn(_(b'wire protocol version 2 encoder referenced in ' | 577 ui.warn( |
528 b'config (%s) is not known; ignoring\n') % encoder) | 578 _( |
579 b'wire protocol version 2 encoder referenced in ' | |
580 b'config (%s) is not known; ignoring\n' | |
581 ) | |
582 % encoder | |
583 ) | |
529 else: | 584 else: |
530 encoders.append(encoder) | 585 encoders.append(encoder) |
531 | 586 |
532 else: | 587 else: |
533 encoders = wireprotoframing.STREAM_ENCODERS_ORDER | 588 encoders = wireprotoframing.STREAM_ENCODERS_ORDER |
534 | 589 |
535 reactor = wireprotoframing.clientreactor(ui, | 590 reactor = wireprotoframing.clientreactor( |
536 hasmultiplesend=False, | 591 ui, |
537 buffersends=True, | 592 hasmultiplesend=False, |
538 clientcontentencoders=encoders) | 593 buffersends=True, |
539 | 594 clientcontentencoders=encoders, |
540 handler = wireprotov2peer.clienthandler(ui, reactor, | 595 ) |
541 opener=opener, | 596 |
542 requestbuilder=requestbuilder) | 597 handler = wireprotov2peer.clienthandler( |
598 ui, reactor, opener=opener, requestbuilder=requestbuilder | |
599 ) | |
543 | 600 |
544 url = '%s/%s' % (apiurl, permission) | 601 url = '%s/%s' % (apiurl, permission) |
545 | 602 |
546 if len(requests) > 1: | 603 if len(requests) > 1: |
547 url += '/multirequest' | 604 url += '/multirequest' |
548 else: | 605 else: |
549 url += '/%s' % requests[0][0] | 606 url += '/%s' % requests[0][0] |
550 | 607 |
551 ui.debug('sending %d commands\n' % len(requests)) | 608 ui.debug('sending %d commands\n' % len(requests)) |
552 for command, args, f in requests: | 609 for command, args, f in requests: |
553 ui.debug('sending command %s: %s\n' % ( | 610 ui.debug( |
554 command, stringutil.pprint(args, indent=2))) | 611 'sending command %s: %s\n' |
555 assert not list(handler.callcommand(command, args, f, | 612 % (command, stringutil.pprint(args, indent=2)) |
556 redirect=redirect)) | 613 ) |
614 assert not list( | |
615 handler.callcommand(command, args, f, redirect=redirect) | |
616 ) | |
557 | 617 |
558 # TODO stream this. | 618 # TODO stream this. |
559 body = b''.join(map(bytes, handler.flushcommands())) | 619 body = b''.join(map(bytes, handler.flushcommands())) |
560 | 620 |
561 # TODO modify user-agent to reflect v2 | 621 # TODO modify user-agent to reflect v2 |
578 ui.traceback() | 638 ui.traceback() |
579 raise IOError(None, e) | 639 raise IOError(None, e) |
580 | 640 |
581 return handler, res | 641 return handler, res |
582 | 642 |
643 | |
583 class queuedcommandfuture(pycompat.futures.Future): | 644 class queuedcommandfuture(pycompat.futures.Future): |
584 """Wraps result() on command futures to trigger submission on call.""" | 645 """Wraps result() on command futures to trigger submission on call.""" |
585 | 646 |
586 def result(self, timeout=None): | 647 def result(self, timeout=None): |
587 if self.done(): | 648 if self.done(): |
591 | 652 |
592 # sendcommands() will restore the original __class__ and self.result | 653 # sendcommands() will restore the original __class__ and self.result |
593 # will resolve to Future.result. | 654 # will resolve to Future.result. |
594 return self.result(timeout) | 655 return self.result(timeout) |
595 | 656 |
657 | |
596 @interfaceutil.implementer(repository.ipeercommandexecutor) | 658 @interfaceutil.implementer(repository.ipeercommandexecutor) |
597 class httpv2executor(object): | 659 class httpv2executor(object): |
598 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor, | 660 def __init__( |
599 redirect): | 661 self, ui, opener, requestbuilder, apiurl, descriptor, redirect |
662 ): | |
600 self._ui = ui | 663 self._ui = ui |
601 self._opener = opener | 664 self._opener = opener |
602 self._requestbuilder = requestbuilder | 665 self._requestbuilder = requestbuilder |
603 self._apiurl = apiurl | 666 self._apiurl = apiurl |
604 self._descriptor = descriptor | 667 self._descriptor = descriptor |
617 def __exit__(self, exctype, excvalue, exctb): | 680 def __exit__(self, exctype, excvalue, exctb): |
618 self.close() | 681 self.close() |
619 | 682 |
620 def callcommand(self, command, args): | 683 def callcommand(self, command, args): |
621 if self._sent: | 684 if self._sent: |
622 raise error.ProgrammingError('callcommand() cannot be used after ' | 685 raise error.ProgrammingError( |
623 'commands are sent') | 686 'callcommand() cannot be used after ' 'commands are sent' |
687 ) | |
624 | 688 |
625 if self._closed: | 689 if self._closed: |
626 raise error.ProgrammingError('callcommand() cannot be used after ' | 690 raise error.ProgrammingError( |
627 'close()') | 691 'callcommand() cannot be used after ' 'close()' |
692 ) | |
628 | 693 |
629 # The service advertises which commands are available. So if we attempt | 694 # The service advertises which commands are available. So if we attempt |
630 # to call an unknown command or pass an unknown argument, we can screen | 695 # to call an unknown command or pass an unknown argument, we can screen |
631 # for this. | 696 # for this. |
632 if command not in self._descriptor['commands']: | 697 if command not in self._descriptor['commands']: |
633 raise error.ProgrammingError( | 698 raise error.ProgrammingError( |
634 'wire protocol command %s is not available' % command) | 699 'wire protocol command %s is not available' % command |
700 ) | |
635 | 701 |
636 cmdinfo = self._descriptor['commands'][command] | 702 cmdinfo = self._descriptor['commands'][command] |
637 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {})) | 703 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {})) |
638 | 704 |
639 if unknownargs: | 705 if unknownargs: |
640 raise error.ProgrammingError( | 706 raise error.ProgrammingError( |
641 'wire protocol command %s does not accept argument: %s' % ( | 707 'wire protocol command %s does not accept argument: %s' |
642 command, ', '.join(sorted(unknownargs)))) | 708 % (command, ', '.join(sorted(unknownargs))) |
709 ) | |
643 | 710 |
644 self._neededpermissions |= set(cmdinfo['permissions']) | 711 self._neededpermissions |= set(cmdinfo['permissions']) |
645 | 712 |
646 # TODO we /could/ also validate types here, since the API descriptor | 713 # TODO we /could/ also validate types here, since the API descriptor |
647 # includes types... | 714 # includes types... |
673 if isinstance(f, queuedcommandfuture): | 740 if isinstance(f, queuedcommandfuture): |
674 f.__class__ = pycompat.futures.Future | 741 f.__class__ = pycompat.futures.Future |
675 f._peerexecutor = None | 742 f._peerexecutor = None |
676 | 743 |
677 # Mark the future as running and filter out cancelled futures. | 744 # Mark the future as running and filter out cancelled futures. |
678 calls = [(command, args, f) | 745 calls = [ |
679 for command, args, f in self._calls | 746 (command, args, f) |
680 if f.set_running_or_notify_cancel()] | 747 for command, args, f in self._calls |
748 if f.set_running_or_notify_cancel() | |
749 ] | |
681 | 750 |
682 # Clear out references, prevent improper object usage. | 751 # Clear out references, prevent improper object usage. |
683 self._calls = None | 752 self._calls = None |
684 | 753 |
685 if not calls: | 754 if not calls: |
689 | 758 |
690 if 'push' in permissions and 'pull' in permissions: | 759 if 'push' in permissions and 'pull' in permissions: |
691 permissions.remove('pull') | 760 permissions.remove('pull') |
692 | 761 |
693 if len(permissions) > 1: | 762 if len(permissions) > 1: |
694 raise error.RepoError(_('cannot make request requiring multiple ' | 763 raise error.RepoError( |
695 'permissions: %s') % | 764 _('cannot make request requiring multiple ' 'permissions: %s') |
696 _(', ').join(sorted(permissions))) | 765 % _(', ').join(sorted(permissions)) |
697 | 766 ) |
698 permission = { | 767 |
699 'push': 'rw', | 768 permission = {'push': 'rw', 'pull': 'ro',}[permissions.pop()] |
700 'pull': 'ro', | |
701 }[permissions.pop()] | |
702 | 769 |
703 handler, resp = sendv2request( | 770 handler, resp = sendv2request( |
704 self._ui, self._opener, self._requestbuilder, self._apiurl, | 771 self._ui, |
705 permission, calls, self._redirect) | 772 self._opener, |
773 self._requestbuilder, | |
774 self._apiurl, | |
775 permission, | |
776 calls, | |
777 self._redirect, | |
778 ) | |
706 | 779 |
707 # TODO we probably want to validate the HTTP code, media type, etc. | 780 # TODO we probably want to validate the HTTP code, media type, etc. |
708 | 781 |
709 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1) | 782 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1) |
710 self._responsef = self._responseexecutor.submit(self._handleresponse, | 783 self._responsef = self._responseexecutor.submit( |
711 handler, resp) | 784 self._handleresponse, handler, resp |
785 ) | |
712 | 786 |
713 def close(self): | 787 def close(self): |
714 if self._closed: | 788 if self._closed: |
715 return | 789 return |
716 | 790 |
732 | 806 |
733 # If any of our futures are still in progress, mark them as | 807 # If any of our futures are still in progress, mark them as |
734 # errored, otherwise a result() could wait indefinitely. | 808 # errored, otherwise a result() could wait indefinitely. |
735 for f in self._futures: | 809 for f in self._futures: |
736 if not f.done(): | 810 if not f.done(): |
737 f.set_exception(error.ResponseError( | 811 f.set_exception( |
738 _('unfulfilled command response'))) | 812 error.ResponseError(_('unfulfilled command response')) |
813 ) | |
739 | 814 |
740 self._futures = None | 815 self._futures = None |
741 | 816 |
742 def _handleresponse(self, handler, resp): | 817 def _handleresponse(self, handler, resp): |
743 # Called in a thread to read the response. | 818 # Called in a thread to read the response. |
744 | 819 |
745 while handler.readdata(resp): | 820 while handler.readdata(resp): |
746 pass | 821 pass |
747 | 822 |
823 | |
748 @interfaceutil.implementer(repository.ipeerv2) | 824 @interfaceutil.implementer(repository.ipeerv2) |
749 class httpv2peer(object): | 825 class httpv2peer(object): |
750 | 826 |
751 limitedarguments = False | 827 limitedarguments = False |
752 | 828 |
753 def __init__(self, ui, repourl, apipath, opener, requestbuilder, | 829 def __init__( |
754 apidescriptor): | 830 self, ui, repourl, apipath, opener, requestbuilder, apidescriptor |
831 ): | |
755 self.ui = ui | 832 self.ui = ui |
756 self.apidescriptor = apidescriptor | 833 self.apidescriptor = apidescriptor |
757 | 834 |
758 if repourl.endswith('/'): | 835 if repourl.endswith('/'): |
759 repourl = repourl[:-1] | 836 repourl = repourl[:-1] |
780 def canpush(self): | 857 def canpush(self): |
781 # TODO change once implemented. | 858 # TODO change once implemented. |
782 return False | 859 return False |
783 | 860 |
784 def close(self): | 861 def close(self): |
785 self.ui.note(_('(sent %d HTTP requests and %d bytes; ' | 862 self.ui.note( |
786 'received %d bytes in responses)\n') % | 863 _( |
787 (self._opener.requestscount, | 864 '(sent %d HTTP requests and %d bytes; ' |
788 self._opener.sentbytescount, | 865 'received %d bytes in responses)\n' |
789 self._opener.receivedbytescount)) | 866 ) |
867 % ( | |
868 self._opener.requestscount, | |
869 self._opener.sentbytescount, | |
870 self._opener.receivedbytescount, | |
871 ) | |
872 ) | |
790 | 873 |
791 # End of ipeerconnection. | 874 # End of ipeerconnection. |
792 | 875 |
793 # Start of ipeercapabilities. | 876 # Start of ipeercapabilities. |
794 | 877 |
800 # Maps to commands that are available. | 883 # Maps to commands that are available. |
801 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'): | 884 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'): |
802 return True | 885 return True |
803 | 886 |
804 # Other concepts. | 887 # Other concepts. |
805 if name in ('bundle2'): | 888 if name in 'bundle2': |
806 return True | 889 return True |
807 | 890 |
808 # Alias command-* to presence of command of that name. | 891 # Alias command-* to presence of command of that name. |
809 if name.startswith('command-'): | 892 if name.startswith('command-'): |
810 return name[len('command-'):] in self.apidescriptor['commands'] | 893 return name[len('command-') :] in self.apidescriptor['commands'] |
811 | 894 |
812 return False | 895 return False |
813 | 896 |
814 def requirecap(self, name, purpose): | 897 def requirecap(self, name, purpose): |
815 if self.capable(name): | 898 if self.capable(name): |
816 return | 899 return |
817 | 900 |
818 raise error.CapabilityError( | 901 raise error.CapabilityError( |
819 _('cannot %s; client or remote repository does not support the ' | 902 _( |
820 '\'%s\' capability') % (purpose, name)) | 903 'cannot %s; client or remote repository does not support the ' |
904 '\'%s\' capability' | |
905 ) | |
906 % (purpose, name) | |
907 ) | |
821 | 908 |
822 # End of ipeercapabilities. | 909 # End of ipeercapabilities. |
823 | 910 |
824 def _call(self, name, **args): | 911 def _call(self, name, **args): |
825 with self.commandexecutor() as e: | 912 with self.commandexecutor() as e: |
826 return e.callcommand(name, args).result() | 913 return e.callcommand(name, args).result() |
827 | 914 |
828 def commandexecutor(self): | 915 def commandexecutor(self): |
829 return httpv2executor(self.ui, self._opener, self._requestbuilder, | 916 return httpv2executor( |
830 self._apiurl, self.apidescriptor, self._redirect) | 917 self.ui, |
918 self._opener, | |
919 self._requestbuilder, | |
920 self._apiurl, | |
921 self.apidescriptor, | |
922 self._redirect, | |
923 ) | |
924 | |
831 | 925 |
832 # Registry of API service names to metadata about peers that handle it. | 926 # Registry of API service names to metadata about peers that handle it. |
833 # | 927 # |
834 # The following keys are meaningful: | 928 # The following keys are meaningful: |
835 # | 929 # |
839 # | 933 # |
840 # priority | 934 # priority |
841 # Integer priority for the service. If we could choose from multiple | 935 # Integer priority for the service. If we could choose from multiple |
842 # services, we choose the one with the highest priority. | 936 # services, we choose the one with the highest priority. |
843 API_PEERS = { | 937 API_PEERS = { |
844 wireprototypes.HTTP_WIREPROTO_V2: { | 938 wireprototypes.HTTP_WIREPROTO_V2: {'init': httpv2peer, 'priority': 50,}, |
845 'init': httpv2peer, | |
846 'priority': 50, | |
847 }, | |
848 } | 939 } |
940 | |
849 | 941 |
850 def performhandshake(ui, url, opener, requestbuilder): | 942 def performhandshake(ui, url, opener, requestbuilder): |
851 # The handshake is a request to the capabilities command. | 943 # The handshake is a request to the capabilities command. |
852 | 944 |
853 caps = None | 945 caps = None |
946 | |
854 def capable(x): | 947 def capable(x): |
855 raise error.ProgrammingError('should not be called') | 948 raise error.ProgrammingError('should not be called') |
856 | 949 |
857 args = {} | 950 args = {} |
858 | 951 |
867 args['headers'] = { | 960 args['headers'] = { |
868 r'X-HgProto-1': r'cbor', | 961 r'X-HgProto-1': r'cbor', |
869 } | 962 } |
870 | 963 |
871 args['headers'].update( | 964 args['headers'].update( |
872 encodevalueinheaders(' '.join(sorted(API_PEERS)), | 965 encodevalueinheaders( |
873 'X-HgUpgrade', | 966 ' '.join(sorted(API_PEERS)), |
874 # We don't know the header limit this early. | 967 'X-HgUpgrade', |
875 # So make it small. | 968 # We don't know the header limit this early. |
876 1024)) | 969 # So make it small. |
877 | 970 1024, |
878 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps, | 971 ) |
879 capable, url, 'capabilities', | 972 ) |
880 args) | 973 |
974 req, requrl, qs = makev1commandrequest( | |
975 ui, requestbuilder, caps, capable, url, 'capabilities', args | |
976 ) | |
881 resp = sendrequest(ui, opener, req) | 977 resp = sendrequest(ui, opener, req) |
882 | 978 |
883 # The server may redirect us to the repo root, stripping the | 979 # The server may redirect us to the repo root, stripping the |
884 # ?cmd=capabilities query string from the URL. The server would likely | 980 # ?cmd=capabilities query string from the URL. The server would likely |
885 # return HTML in this case and ``parsev1commandresponse()`` would raise. | 981 # return HTML in this case and ``parsev1commandresponse()`` would raise. |
891 # However, Mercurial clients for several years appeared to handle this | 987 # However, Mercurial clients for several years appeared to handle this |
892 # issue without behavior degradation. And according to issue 5860, it may | 988 # issue without behavior degradation. And according to issue 5860, it may |
893 # be a longstanding bug in some server implementations. So we allow a | 989 # be a longstanding bug in some server implementations. So we allow a |
894 # redirect that drops the query string to "just work." | 990 # redirect that drops the query string to "just work." |
895 try: | 991 try: |
896 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp, | 992 respurl, ct, resp = parsev1commandresponse( |
897 compressible=False, | 993 ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2 |
898 allowcbor=advertisev2) | 994 ) |
899 except RedirectedRepoError as e: | 995 except RedirectedRepoError as e: |
900 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps, | 996 req, requrl, qs = makev1commandrequest( |
901 capable, e.respurl, | 997 ui, requestbuilder, caps, capable, e.respurl, 'capabilities', args |
902 'capabilities', args) | 998 ) |
903 resp = sendrequest(ui, opener, req) | 999 resp = sendrequest(ui, opener, req) |
904 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp, | 1000 respurl, ct, resp = parsev1commandresponse( |
905 compressible=False, | 1001 ui, url, requrl, qs, resp, compressible=False, allowcbor=advertisev2 |
906 allowcbor=advertisev2) | 1002 ) |
907 | 1003 |
908 try: | 1004 try: |
909 rawdata = resp.read() | 1005 rawdata = resp.read() |
910 finally: | 1006 finally: |
911 resp.close() | 1007 resp.close() |
916 if advertisev2: | 1012 if advertisev2: |
917 if ct == 'application/mercurial-cbor': | 1013 if ct == 'application/mercurial-cbor': |
918 try: | 1014 try: |
919 info = cborutil.decodeall(rawdata)[0] | 1015 info = cborutil.decodeall(rawdata)[0] |
920 except cborutil.CBORDecodeError: | 1016 except cborutil.CBORDecodeError: |
921 raise error.Abort(_('error decoding CBOR from remote server'), | 1017 raise error.Abort( |
922 hint=_('try again and consider contacting ' | 1018 _('error decoding CBOR from remote server'), |
923 'the server operator')) | 1019 hint=_( |
1020 'try again and consider contacting ' | |
1021 'the server operator' | |
1022 ), | |
1023 ) | |
924 | 1024 |
925 # We got a legacy response. That's fine. | 1025 # We got a legacy response. That's fine. |
926 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'): | 1026 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'): |
927 info = { | 1027 info = {'v1capabilities': set(rawdata.split())} |
928 'v1capabilities': set(rawdata.split()) | |
929 } | |
930 | 1028 |
931 else: | 1029 else: |
932 raise error.RepoError( | 1030 raise error.RepoError( |
933 _('unexpected response type from server: %s') % ct) | 1031 _('unexpected response type from server: %s') % ct |
1032 ) | |
934 else: | 1033 else: |
935 info = { | 1034 info = {'v1capabilities': set(rawdata.split())} |
936 'v1capabilities': set(rawdata.split()) | |
937 } | |
938 | 1035 |
939 return respurl, info | 1036 return respurl, info |
1037 | |
940 | 1038 |
941 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request): | 1039 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request): |
942 """Construct an appropriate HTTP peer instance. | 1040 """Construct an appropriate HTTP peer instance. |
943 | 1041 |
944 ``opener`` is an ``url.opener`` that should be used to establish | 1042 ``opener`` is an ``url.opener`` that should be used to establish |
947 ``requestbuilder`` is the type used for constructing HTTP requests. | 1045 ``requestbuilder`` is the type used for constructing HTTP requests. |
948 It exists as an argument so extensions can override the default. | 1046 It exists as an argument so extensions can override the default. |
949 """ | 1047 """ |
950 u = util.url(path) | 1048 u = util.url(path) |
951 if u.query or u.fragment: | 1049 if u.query or u.fragment: |
952 raise error.Abort(_('unsupported URL component: "%s"') % | 1050 raise error.Abort( |
953 (u.query or u.fragment)) | 1051 _('unsupported URL component: "%s"') % (u.query or u.fragment) |
1052 ) | |
954 | 1053 |
955 # urllib cannot handle URLs with embedded user or passwd. | 1054 # urllib cannot handle URLs with embedded user or passwd. |
956 url, authinfo = u.authinfo() | 1055 url, authinfo = u.authinfo() |
957 ui.debug('using %s\n' % url) | 1056 ui.debug('using %s\n' % url) |
958 | 1057 |
969 # capabilities, we could filter out services not meeting the | 1068 # capabilities, we could filter out services not meeting the |
970 # requirements. Possibly by consulting the interfaces defined by the | 1069 # requirements. Possibly by consulting the interfaces defined by the |
971 # peer type. | 1070 # peer type. |
972 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys()) | 1071 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys()) |
973 | 1072 |
974 preferredchoices = sorted(apipeerchoices, | 1073 preferredchoices = sorted( |
975 key=lambda x: API_PEERS[x]['priority'], | 1074 apipeerchoices, key=lambda x: API_PEERS[x]['priority'], reverse=True |
976 reverse=True) | 1075 ) |
977 | 1076 |
978 for service in preferredchoices: | 1077 for service in preferredchoices: |
979 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service) | 1078 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service) |
980 | 1079 |
981 return API_PEERS[service]['init'](ui, respurl, apipath, opener, | 1080 return API_PEERS[service]['init']( |
982 requestbuilder, | 1081 ui, respurl, apipath, opener, requestbuilder, info['apis'][service] |
983 info['apis'][service]) | 1082 ) |
984 | 1083 |
985 # Failed to construct an API peer. Fall back to legacy. | 1084 # Failed to construct an API peer. Fall back to legacy. |
986 return httppeer(ui, path, respurl, opener, requestbuilder, | 1085 return httppeer( |
987 info['v1capabilities']) | 1086 ui, path, respurl, opener, requestbuilder, info['v1capabilities'] |
1087 ) | |
1088 | |
988 | 1089 |
989 def instance(ui, path, create, intents=None, createopts=None): | 1090 def instance(ui, path, create, intents=None, createopts=None): |
990 if create: | 1091 if create: |
991 raise error.Abort(_('cannot create new http repository')) | 1092 raise error.Abort(_('cannot create new http repository')) |
992 try: | 1093 try: |
993 if path.startswith('https:') and not urlmod.has_https: | 1094 if path.startswith('https:') and not urlmod.has_https: |
994 raise error.Abort(_('Python support for SSL and HTTPS ' | 1095 raise error.Abort( |
995 'is not installed')) | 1096 _('Python support for SSL and HTTPS ' 'is not installed') |
1097 ) | |
996 | 1098 |
997 inst = makepeer(ui, path) | 1099 inst = makepeer(ui, path) |
998 | 1100 |
999 return inst | 1101 return inst |
1000 except error.RepoError as httpexception: | 1102 except error.RepoError as httpexception: |
1001 try: | 1103 try: |
1002 r = statichttprepo.instance(ui, "static-" + path, create) | 1104 r = statichttprepo.instance(ui, "static-" + path, create) |
1003 ui.note(_('(falling back to static-http)\n')) | 1105 ui.note(_('(falling back to static-http)\n')) |
1004 return r | 1106 return r |
1005 except error.RepoError: | 1107 except error.RepoError: |
1006 raise httpexception # use the original http RepoError instead | 1108 raise httpexception # use the original http RepoError instead |