28 |
28 |
29 def _serverquote(s): |
29 def _serverquote(s): |
30 """quote a string for the remote shell ... which we assume is sh""" |
30 """quote a string for the remote shell ... which we assume is sh""" |
31 if not s: |
31 if not s: |
32 return s |
32 return s |
33 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s): |
33 if re.match(b'[a-zA-Z0-9@%_+=:,./-]*$', s): |
34 return s |
34 return s |
35 return "'%s'" % s.replace("'", "'\\''") |
35 return b"'%s'" % s.replace(b"'", b"'\\''") |
36 |
36 |
37 |
37 |
38 def _forwardoutput(ui, pipe): |
38 def _forwardoutput(ui, pipe): |
39 """display all data currently available on pipe as remote output. |
39 """display all data currently available on pipe as remote output. |
40 |
40 |
41 This is non blocking.""" |
41 This is non blocking.""" |
42 if pipe: |
42 if pipe: |
43 s = procutil.readpipe(pipe) |
43 s = procutil.readpipe(pipe) |
44 if s: |
44 if s: |
45 for l in s.splitlines(): |
45 for l in s.splitlines(): |
46 ui.status(_("remote: "), l, '\n') |
46 ui.status(_(b"remote: "), l, b'\n') |
47 |
47 |
48 |
48 |
49 class doublepipe(object): |
49 class doublepipe(object): |
50 """Operate a side-channel pipe in addition of a main one |
50 """Operate a side-channel pipe in addition of a main one |
51 |
51 |
89 # non supported yet case, assume all have data. |
89 # non supported yet case, assume all have data. |
90 act = fds |
90 act = fds |
91 return (self._main.fileno() in act, self._side.fileno() in act) |
91 return (self._main.fileno() in act, self._side.fileno() in act) |
92 |
92 |
93 def write(self, data): |
93 def write(self, data): |
94 return self._call('write', data) |
94 return self._call(b'write', data) |
95 |
95 |
96 def read(self, size): |
96 def read(self, size): |
97 r = self._call('read', size) |
97 r = self._call(b'read', size) |
98 if size != 0 and not r: |
98 if size != 0 and not r: |
99 # We've observed a condition that indicates the |
99 # We've observed a condition that indicates the |
100 # stdout closed unexpectedly. Check stderr one |
100 # stdout closed unexpectedly. Check stderr one |
101 # more time and snag anything that's there before |
101 # more time and snag anything that's there before |
102 # letting anyone know the main part of the pipe |
102 # letting anyone know the main part of the pipe |
103 # closed prematurely. |
103 # closed prematurely. |
104 _forwardoutput(self._ui, self._side) |
104 _forwardoutput(self._ui, self._side) |
105 return r |
105 return r |
106 |
106 |
107 def unbufferedread(self, size): |
107 def unbufferedread(self, size): |
108 r = self._call('unbufferedread', size) |
108 r = self._call(b'unbufferedread', size) |
109 if size != 0 and not r: |
109 if size != 0 and not r: |
110 # We've observed a condition that indicates the |
110 # We've observed a condition that indicates the |
111 # stdout closed unexpectedly. Check stderr one |
111 # stdout closed unexpectedly. Check stderr one |
112 # more time and snag anything that's there before |
112 # more time and snag anything that's there before |
113 # letting anyone know the main part of the pipe |
113 # letting anyone know the main part of the pipe |
114 # closed prematurely. |
114 # closed prematurely. |
115 _forwardoutput(self._ui, self._side) |
115 _forwardoutput(self._ui, self._side) |
116 return r |
116 return r |
117 |
117 |
118 def readline(self): |
118 def readline(self): |
119 return self._call('readline') |
119 return self._call(b'readline') |
120 |
120 |
121 def _call(self, methname, data=None): |
121 def _call(self, methname, data=None): |
122 """call <methname> on "main", forward output of "side" while blocking |
122 """call <methname> on "main", forward output of "side" while blocking |
123 """ |
123 """ |
124 # data can be '' or 0 |
124 # data can be '' or 0 |
125 if (data is not None and not data) or self._main.closed: |
125 if (data is not None and not data) or self._main.closed: |
126 _forwardoutput(self._ui, self._side) |
126 _forwardoutput(self._ui, self._side) |
127 return '' |
127 return b'' |
128 while True: |
128 while True: |
129 mainready, sideready = self._wait() |
129 mainready, sideready = self._wait() |
130 if sideready: |
130 if sideready: |
131 _forwardoutput(self._ui, self._side) |
131 _forwardoutput(self._ui, self._side) |
132 if mainready: |
132 if mainready: |
190 def _clientcapabilities(): |
190 def _clientcapabilities(): |
191 """Return list of capabilities of this client. |
191 """Return list of capabilities of this client. |
192 |
192 |
193 Returns a list of capabilities that are supported by this client. |
193 Returns a list of capabilities that are supported by this client. |
194 """ |
194 """ |
195 protoparams = {'partial-pull'} |
195 protoparams = {b'partial-pull'} |
196 comps = [ |
196 comps = [ |
197 e.wireprotosupport().name |
197 e.wireprotosupport().name |
198 for e in util.compengines.supportedwireengines(util.CLIENTROLE) |
198 for e in util.compengines.supportedwireengines(util.CLIENTROLE) |
199 ] |
199 ] |
200 protoparams.add('comp=%s' % ','.join(comps)) |
200 protoparams.add(b'comp=%s' % b','.join(comps)) |
201 return protoparams |
201 return protoparams |
202 |
202 |
203 |
203 |
204 def _performhandshake(ui, stdin, stdout, stderr): |
204 def _performhandshake(ui, stdin, stdout, stderr): |
205 def badresponse(): |
205 def badresponse(): |
206 # Flush any output on stderr. |
206 # Flush any output on stderr. |
207 _forwardoutput(ui, stderr) |
207 _forwardoutput(ui, stderr) |
208 |
208 |
209 msg = _('no suitable response from remote hg') |
209 msg = _(b'no suitable response from remote hg') |
210 hint = ui.config('ui', 'ssherrorhint') |
210 hint = ui.config(b'ui', b'ssherrorhint') |
211 raise error.RepoError(msg, hint=hint) |
211 raise error.RepoError(msg, hint=hint) |
212 |
212 |
213 # The handshake consists of sending wire protocol commands in reverse |
213 # The handshake consists of sending wire protocol commands in reverse |
214 # order of protocol implementation and then sniffing for a response |
214 # order of protocol implementation and then sniffing for a response |
215 # to one of them. |
215 # to one of them. |
260 # print messages to stdout on login. Issuing commands on connection |
260 # print messages to stdout on login. Issuing commands on connection |
261 # allows us to flush this banner output from the server by scanning |
261 # allows us to flush this banner output from the server by scanning |
262 # for output to our well-known ``between`` command. Of course, if |
262 # for output to our well-known ``between`` command. Of course, if |
263 # the banner contains ``1\n\n``, this will throw off our detection. |
263 # the banner contains ``1\n\n``, this will throw off our detection. |
264 |
264 |
265 requestlog = ui.configbool('devel', 'debug.peer-request') |
265 requestlog = ui.configbool(b'devel', b'debug.peer-request') |
266 |
266 |
267 # Generate a random token to help identify responses to version 2 |
267 # Generate a random token to help identify responses to version 2 |
268 # upgrade request. |
268 # upgrade request. |
269 token = pycompat.sysbytes(str(uuid.uuid4())) |
269 token = pycompat.sysbytes(str(uuid.uuid4())) |
270 upgradecaps = [ |
270 upgradecaps = [ |
271 ('proto', wireprotoserver.SSHV2), |
271 (b'proto', wireprotoserver.SSHV2), |
272 ] |
272 ] |
273 upgradecaps = util.urlreq.urlencode(upgradecaps) |
273 upgradecaps = util.urlreq.urlencode(upgradecaps) |
274 |
274 |
275 try: |
275 try: |
276 pairsarg = '%s-%s' % ('0' * 40, '0' * 40) |
276 pairsarg = b'%s-%s' % (b'0' * 40, b'0' * 40) |
277 handshake = [ |
277 handshake = [ |
278 'hello\n', |
278 b'hello\n', |
279 'between\n', |
279 b'between\n', |
280 'pairs %d\n' % len(pairsarg), |
280 b'pairs %d\n' % len(pairsarg), |
281 pairsarg, |
281 pairsarg, |
282 ] |
282 ] |
283 |
283 |
284 # Request upgrade to version 2 if configured. |
284 # Request upgrade to version 2 if configured. |
285 if ui.configbool('experimental', 'sshpeer.advertise-v2'): |
285 if ui.configbool(b'experimental', b'sshpeer.advertise-v2'): |
286 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps)) |
286 ui.debug(b'sending upgrade request: %s %s\n' % (token, upgradecaps)) |
287 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps)) |
287 handshake.insert(0, b'upgrade %s %s\n' % (token, upgradecaps)) |
288 |
288 |
289 if requestlog: |
289 if requestlog: |
290 ui.debug('devel-peer-request: hello+between\n') |
290 ui.debug(b'devel-peer-request: hello+between\n') |
291 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg)) |
291 ui.debug(b'devel-peer-request: pairs: %d bytes\n' % len(pairsarg)) |
292 ui.debug('sending hello command\n') |
292 ui.debug(b'sending hello command\n') |
293 ui.debug('sending between command\n') |
293 ui.debug(b'sending between command\n') |
294 |
294 |
295 stdin.write(''.join(handshake)) |
295 stdin.write(b''.join(handshake)) |
296 stdin.flush() |
296 stdin.flush() |
297 except IOError: |
297 except IOError: |
298 badresponse() |
298 badresponse() |
299 |
299 |
300 # Assume version 1 of wire protocol by default. |
300 # Assume version 1 of wire protocol by default. |
301 protoname = wireprototypes.SSHV1 |
301 protoname = wireprototypes.SSHV1 |
302 reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token)) |
302 reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token)) |
303 |
303 |
304 lines = ['', 'dummy'] |
304 lines = [b'', b'dummy'] |
305 max_noise = 500 |
305 max_noise = 500 |
306 while lines[-1] and max_noise: |
306 while lines[-1] and max_noise: |
307 try: |
307 try: |
308 l = stdout.readline() |
308 l = stdout.readline() |
309 _forwardoutput(ui, stderr) |
309 _forwardoutput(ui, stderr) |
311 # Look for reply to protocol upgrade request. It has a token |
311 # Look for reply to protocol upgrade request. It has a token |
312 # in it, so there should be no false positives. |
312 # in it, so there should be no false positives. |
313 m = reupgraded.match(l) |
313 m = reupgraded.match(l) |
314 if m: |
314 if m: |
315 protoname = m.group(1) |
315 protoname = m.group(1) |
316 ui.debug('protocol upgraded to %s\n' % protoname) |
316 ui.debug(b'protocol upgraded to %s\n' % protoname) |
317 # If an upgrade was handled, the ``hello`` and ``between`` |
317 # If an upgrade was handled, the ``hello`` and ``between`` |
318 # requests are ignored. The next output belongs to the |
318 # requests are ignored. The next output belongs to the |
319 # protocol, so stop scanning lines. |
319 # protocol, so stop scanning lines. |
320 break |
320 break |
321 |
321 |
322 # Otherwise it could be a banner, ``0\n`` response if server |
322 # Otherwise it could be a banner, ``0\n`` response if server |
323 # doesn't support upgrade. |
323 # doesn't support upgrade. |
324 |
324 |
325 if lines[-1] == '1\n' and l == '\n': |
325 if lines[-1] == b'1\n' and l == b'\n': |
326 break |
326 break |
327 if l: |
327 if l: |
328 ui.debug('remote: ', l) |
328 ui.debug(b'remote: ', l) |
329 lines.append(l) |
329 lines.append(l) |
330 max_noise -= 1 |
330 max_noise -= 1 |
331 except IOError: |
331 except IOError: |
332 badresponse() |
332 badresponse() |
333 else: |
333 else: |
453 |
453 |
454 __del__ = _cleanup |
454 __del__ = _cleanup |
455 |
455 |
456 def _sendrequest(self, cmd, args, framed=False): |
456 def _sendrequest(self, cmd, args, framed=False): |
457 if self.ui.debugflag and self.ui.configbool( |
457 if self.ui.debugflag and self.ui.configbool( |
458 'devel', 'debug.peer-request' |
458 b'devel', b'debug.peer-request' |
459 ): |
459 ): |
460 dbg = self.ui.debug |
460 dbg = self.ui.debug |
461 line = 'devel-peer-request: %s\n' |
461 line = b'devel-peer-request: %s\n' |
462 dbg(line % cmd) |
462 dbg(line % cmd) |
463 for key, value in sorted(args.items()): |
463 for key, value in sorted(args.items()): |
464 if not isinstance(value, dict): |
464 if not isinstance(value, dict): |
465 dbg(line % ' %s: %d bytes' % (key, len(value))) |
465 dbg(line % b' %s: %d bytes' % (key, len(value))) |
466 else: |
466 else: |
467 for dk, dv in sorted(value.items()): |
467 for dk, dv in sorted(value.items()): |
468 dbg(line % ' %s-%s: %d' % (key, dk, len(dv))) |
468 dbg(line % b' %s-%s: %d' % (key, dk, len(dv))) |
469 self.ui.debug("sending %s command\n" % cmd) |
469 self.ui.debug(b"sending %s command\n" % cmd) |
470 self._pipeo.write("%s\n" % cmd) |
470 self._pipeo.write(b"%s\n" % cmd) |
471 _func, names = wireprotov1server.commands[cmd] |
471 _func, names = wireprotov1server.commands[cmd] |
472 keys = names.split() |
472 keys = names.split() |
473 wireargs = {} |
473 wireargs = {} |
474 for k in keys: |
474 for k in keys: |
475 if k == '*': |
475 if k == b'*': |
476 wireargs['*'] = args |
476 wireargs[b'*'] = args |
477 break |
477 break |
478 else: |
478 else: |
479 wireargs[k] = args[k] |
479 wireargs[k] = args[k] |
480 del args[k] |
480 del args[k] |
481 for k, v in sorted(wireargs.iteritems()): |
481 for k, v in sorted(wireargs.iteritems()): |
482 self._pipeo.write("%s %d\n" % (k, len(v))) |
482 self._pipeo.write(b"%s %d\n" % (k, len(v))) |
483 if isinstance(v, dict): |
483 if isinstance(v, dict): |
484 for dk, dv in v.iteritems(): |
484 for dk, dv in v.iteritems(): |
485 self._pipeo.write("%s %d\n" % (dk, len(dv))) |
485 self._pipeo.write(b"%s %d\n" % (dk, len(dv))) |
486 self._pipeo.write(dv) |
486 self._pipeo.write(dv) |
487 else: |
487 else: |
488 self._pipeo.write(v) |
488 self._pipeo.write(v) |
489 self._pipeo.flush() |
489 self._pipeo.flush() |
490 |
490 |
513 def _callpush(self, cmd, fp, **args): |
513 def _callpush(self, cmd, fp, **args): |
514 # The server responds with an empty frame if the client should |
514 # The server responds with an empty frame if the client should |
515 # continue submitting the payload. |
515 # continue submitting the payload. |
516 r = self._call(cmd, **args) |
516 r = self._call(cmd, **args) |
517 if r: |
517 if r: |
518 return '', r |
518 return b'', r |
519 |
519 |
520 # The payload consists of frames with content followed by an empty |
520 # The payload consists of frames with content followed by an empty |
521 # frame. |
521 # frame. |
522 for d in iter(lambda: fp.read(4096), ''): |
522 for d in iter(lambda: fp.read(4096), b''): |
523 self._writeframed(d) |
523 self._writeframed(d) |
524 self._writeframed("", flush=True) |
524 self._writeframed(b"", flush=True) |
525 |
525 |
526 # In case of success, there is an empty frame and a frame containing |
526 # In case of success, there is an empty frame and a frame containing |
527 # the integer result (as a string). |
527 # the integer result (as a string). |
528 # In case of error, there is a non-empty frame containing the error. |
528 # In case of error, there is a non-empty frame containing the error. |
529 r = self._readframed() |
529 r = self._readframed() |
530 if r: |
530 if r: |
531 return '', r |
531 return b'', r |
532 return self._readframed(), '' |
532 return self._readframed(), b'' |
533 |
533 |
534 def _calltwowaystream(self, cmd, fp, **args): |
534 def _calltwowaystream(self, cmd, fp, **args): |
535 # The server responds with an empty frame if the client should |
535 # The server responds with an empty frame if the client should |
536 # continue submitting the payload. |
536 # continue submitting the payload. |
537 r = self._call(cmd, **args) |
537 r = self._call(cmd, **args) |
538 if r: |
538 if r: |
539 # XXX needs to be made better |
539 # XXX needs to be made better |
540 raise error.Abort(_('unexpected remote reply: %s') % r) |
540 raise error.Abort(_(b'unexpected remote reply: %s') % r) |
541 |
541 |
542 # The payload consists of frames with content followed by an empty |
542 # The payload consists of frames with content followed by an empty |
543 # frame. |
543 # frame. |
544 for d in iter(lambda: fp.read(4096), ''): |
544 for d in iter(lambda: fp.read(4096), b''): |
545 self._writeframed(d) |
545 self._writeframed(d) |
546 self._writeframed("", flush=True) |
546 self._writeframed(b"", flush=True) |
547 |
547 |
548 return self._pipei |
548 return self._pipei |
549 |
549 |
550 def _getamount(self): |
550 def _getamount(self): |
551 l = self._pipei.readline() |
551 l = self._pipei.readline() |
552 if l == '\n': |
552 if l == b'\n': |
553 if self._autoreadstderr: |
553 if self._autoreadstderr: |
554 self._readerr() |
554 self._readerr() |
555 msg = _('check previous remote output') |
555 msg = _(b'check previous remote output') |
556 self._abort(error.OutOfBandError(hint=msg)) |
556 self._abort(error.OutOfBandError(hint=msg)) |
557 if self._autoreadstderr: |
557 if self._autoreadstderr: |
558 self._readerr() |
558 self._readerr() |
559 try: |
559 try: |
560 return int(l) |
560 return int(l) |
561 except ValueError: |
561 except ValueError: |
562 self._abort(error.ResponseError(_("unexpected response:"), l)) |
562 self._abort(error.ResponseError(_(b"unexpected response:"), l)) |
563 |
563 |
564 def _readframed(self): |
564 def _readframed(self): |
565 size = self._getamount() |
565 size = self._getamount() |
566 if not size: |
566 if not size: |
567 return b'' |
567 return b'' |
568 |
568 |
569 return self._pipei.read(size) |
569 return self._pipei.read(size) |
570 |
570 |
571 def _writeframed(self, data, flush=False): |
571 def _writeframed(self, data, flush=False): |
572 self._pipeo.write("%d\n" % len(data)) |
572 self._pipeo.write(b"%d\n" % len(data)) |
573 if data: |
573 if data: |
574 self._pipeo.write(data) |
574 self._pipeo.write(data) |
575 if flush: |
575 if flush: |
576 self._pipeo.flush() |
576 self._pipeo.flush() |
577 if self._autoreadstderr: |
577 if self._autoreadstderr: |
629 autoreadstderr=autoreadstderr, |
629 autoreadstderr=autoreadstderr, |
630 ) |
630 ) |
631 else: |
631 else: |
632 _cleanuppipes(ui, stdout, stdin, stderr) |
632 _cleanuppipes(ui, stdout, stdin, stderr) |
633 raise error.RepoError( |
633 raise error.RepoError( |
634 _('unknown version of SSH protocol: %s') % protoname |
634 _(b'unknown version of SSH protocol: %s') % protoname |
635 ) |
635 ) |
636 |
636 |
637 |
637 |
638 def instance(ui, path, create, intents=None, createopts=None): |
638 def instance(ui, path, create, intents=None, createopts=None): |
639 """Create an SSH peer. |
639 """Create an SSH peer. |
640 |
640 |
641 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface. |
641 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface. |
642 """ |
642 """ |
643 u = util.url(path, parsequery=False, parsefragment=False) |
643 u = util.url(path, parsequery=False, parsefragment=False) |
644 if u.scheme != 'ssh' or not u.host or u.path is None: |
644 if u.scheme != b'ssh' or not u.host or u.path is None: |
645 raise error.RepoError(_("couldn't parse location %s") % path) |
645 raise error.RepoError(_(b"couldn't parse location %s") % path) |
646 |
646 |
647 util.checksafessh(path) |
647 util.checksafessh(path) |
648 |
648 |
649 if u.passwd is not None: |
649 if u.passwd is not None: |
650 raise error.RepoError(_('password in URL not supported')) |
650 raise error.RepoError(_(b'password in URL not supported')) |
651 |
651 |
652 sshcmd = ui.config('ui', 'ssh') |
652 sshcmd = ui.config(b'ui', b'ssh') |
653 remotecmd = ui.config('ui', 'remotecmd') |
653 remotecmd = ui.config(b'ui', b'remotecmd') |
654 sshaddenv = dict(ui.configitems('sshenv')) |
654 sshaddenv = dict(ui.configitems(b'sshenv')) |
655 sshenv = procutil.shellenviron(sshaddenv) |
655 sshenv = procutil.shellenviron(sshaddenv) |
656 remotepath = u.path or '.' |
656 remotepath = u.path or b'.' |
657 |
657 |
658 args = procutil.sshargs(sshcmd, u.host, u.user, u.port) |
658 args = procutil.sshargs(sshcmd, u.host, u.user, u.port) |
659 |
659 |
660 if create: |
660 if create: |
661 # We /could/ do this, but only if the remote init command knows how to |
661 # We /could/ do this, but only if the remote init command knows how to |
662 # handle them. We don't yet make any assumptions about that. And without |
662 # handle them. We don't yet make any assumptions about that. And without |
663 # querying the remote, there's no way of knowing if the remote even |
663 # querying the remote, there's no way of knowing if the remote even |
664 # supports said requested feature. |
664 # supports said requested feature. |
665 if createopts: |
665 if createopts: |
666 raise error.RepoError( |
666 raise error.RepoError( |
667 _('cannot create remote SSH repositories ' 'with extra options') |
667 _( |
|
668 b'cannot create remote SSH repositories ' |
|
669 b'with extra options' |
|
670 ) |
668 ) |
671 ) |
669 |
672 |
670 cmd = '%s %s %s' % ( |
673 cmd = b'%s %s %s' % ( |
671 sshcmd, |
674 sshcmd, |
672 args, |
675 args, |
673 procutil.shellquote( |
676 procutil.shellquote( |
674 '%s init %s' |
677 b'%s init %s' |
675 % (_serverquote(remotecmd), _serverquote(remotepath)) |
678 % (_serverquote(remotecmd), _serverquote(remotepath)) |
676 ), |
679 ), |
677 ) |
680 ) |
678 ui.debug('running %s\n' % cmd) |
681 ui.debug(b'running %s\n' % cmd) |
679 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv) |
682 res = ui.system(cmd, blockedtag=b'sshpeer', environ=sshenv) |
680 if res != 0: |
683 if res != 0: |
681 raise error.RepoError(_('could not create remote repo')) |
684 raise error.RepoError(_(b'could not create remote repo')) |
682 |
685 |
683 proc, stdin, stdout, stderr = _makeconnection( |
686 proc, stdin, stdout, stderr = _makeconnection( |
684 ui, sshcmd, args, remotecmd, remotepath, sshenv |
687 ui, sshcmd, args, remotecmd, remotepath, sshenv |
685 ) |
688 ) |
686 |
689 |
687 peer = makepeer(ui, path, proc, stdin, stdout, stderr) |
690 peer = makepeer(ui, path, proc, stdin, stdout, stderr) |
688 |
691 |
689 # Finally, if supported by the server, notify it about our own |
692 # Finally, if supported by the server, notify it about our own |
690 # capabilities. |
693 # capabilities. |
691 if 'protocaps' in peer.capabilities(): |
694 if b'protocaps' in peer.capabilities(): |
692 try: |
695 try: |
693 peer._call( |
696 peer._call( |
694 "protocaps", caps=' '.join(sorted(_clientcapabilities())) |
697 b"protocaps", caps=b' '.join(sorted(_clientcapabilities())) |
695 ) |
698 ) |
696 except IOError: |
699 except IOError: |
697 peer._cleanup() |
700 peer._cleanup() |
698 raise error.RepoError(_('capability exchange failed')) |
701 raise error.RepoError(_(b'capability exchange failed')) |
699 |
702 |
700 return peer |
703 return peer |