wireproto.py
1091 lines
| 39.2 KiB
| text/x-python
|
PythonLexer
/ mercurial / wireproto.py
Matt Mackall
|
r11581 | # wireproto.py - generic wire protocol support functions | ||
# | ||||
# Copyright 2005-2010 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Gregory Szorc
|
r25993 | from __future__ import absolute_import | ||
Augie Fackler
|
r29341 | import hashlib | ||
Gregory Szorc
|
r25993 | import os | ||
import tempfile | ||||
Matt Mackall
|
r11581 | |||
Gregory Szorc
|
r25993 | from .i18n import _ | ||
from .node import ( | ||||
bin, | ||||
hex, | ||||
Siddharth Agarwal
|
r32260 | nullid, | ||
Gregory Szorc
|
r25993 | ) | ||
from . import ( | ||||
bundle2, | ||||
changegroup as changegroupmod, | ||||
Durham Goode
|
r34099 | discovery, | ||
Gregory Szorc
|
r25993 | encoding, | ||
error, | ||||
exchange, | ||||
peer, | ||||
pushkey as pushkeymod, | ||||
Pulkit Goyal
|
r30924 | pycompat, | ||
Gregory Szorc
|
r33805 | repository, | ||
Gregory Szorc
|
r26443 | streamclone, | ||
Gregory Szorc
|
r25993 | util, | ||
Gregory Szorc
|
r36090 | wireprototypes, | ||
Gregory Szorc
|
r25993 | ) | ||
Pierre-Yves David
|
r20903 | |||
timeless
|
r28883 | urlerr = util.urlerr | ||
urlreq = util.urlreq | ||||
Gregory Szorc
|
r36091 | bytesresponse = wireprototypes.bytesresponse | ||
Gregory Szorc
|
r36090 | ooberror = wireprototypes.ooberror | ||
pushres = wireprototypes.pushres | ||||
pusherr = wireprototypes.pusherr | ||||
streamres = wireprototypes.streamres | ||||
streamres_legacy = wireprototypes.streamreslegacy | ||||
Pierre-Yves David
|
r30909 | bundle2requiredmain = _('incompatible Mercurial client; bundle2 required') | ||
bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/' | ||||
'IncompatibleClient') | ||||
bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint) | ||||
Gregory Szorc
|
r27246 | |||
Augie Fackler
|
r28436 | class remoteiterbatcher(peer.iterbatcher): | ||
def __init__(self, remote): | ||||
super(remoteiterbatcher, self).__init__() | ||||
self._remote = remote | ||||
Augie Fackler
|
r28438 | def __getattr__(self, name): | ||
Gregory Szorc
|
r33759 | # Validate this method is batchable, since submit() only supports | ||
# batchable methods. | ||||
fn = getattr(self._remote, name) | ||||
if not getattr(fn, 'batchable', None): | ||||
raise error.ProgrammingError('Attempted to batch a non-batchable ' | ||||
'call to %r' % name) | ||||
Augie Fackler
|
r28438 | return super(remoteiterbatcher, self).__getattr__(name) | ||
Augie Fackler
|
r28436 | def submit(self): | ||
"""Break the batch request into many patch calls and pipeline them. | ||||
This is mostly valuable over http where request sizes can be | ||||
limited, but can be used in other places as well. | ||||
""" | ||||
Gregory Szorc
|
r33761 | # 2-tuple of (command, arguments) that represents what will be | ||
# sent over the wire. | ||||
requests = [] | ||||
# 4-tuple of (command, final future, @batchable generator, remote | ||||
# future). | ||||
results = [] | ||||
for command, args, opts, finalfuture in self.calls: | ||||
mtd = getattr(self._remote, command) | ||||
Augie Fackler
|
r34727 | batchable = mtd.batchable(mtd.__self__, *args, **opts) | ||
Gregory Szorc
|
r33761 | |||
commandargs, fremote = next(batchable) | ||||
assert fremote | ||||
requests.append((command, commandargs)) | ||||
results.append((command, finalfuture, batchable, fremote)) | ||||
if requests: | ||||
self._resultiter = self._remote._submitbatch(requests) | ||||
self._results = results | ||||
Augie Fackler
|
r28436 | |||
def results(self): | ||||
Gregory Szorc
|
r33761 | for command, finalfuture, batchable, remotefuture in self._results: | ||
# Get the raw result, set it in the remote future, feed it | ||||
# back into the @batchable generator so it can be decoded, and | ||||
# set the result on the final future to this value. | ||||
remoteresult = next(self._resultiter) | ||||
remotefuture.set(remoteresult) | ||||
finalfuture.set(next(batchable)) | ||||
# Verify our @batchable generators only emit 2 values. | ||||
try: | ||||
next(batchable) | ||||
except StopIteration: | ||||
pass | ||||
else: | ||||
raise error.ProgrammingError('%s @batchable generator emitted ' | ||||
'unexpected value count' % command) | ||||
yield finalfuture.value | ||||
Augie Fackler
|
r28436 | |||
Augie Fackler
|
r25912 | # Forward a couple of names from peer to make wireproto interactions | ||
# slightly more sensible. | ||||
batchable = peer.batchable | ||||
future = peer.future | ||||
Peter Arrenbrecht
|
r14621 | |||
Benoit Boissinot
|
r11597 | # list of nodes encoding / decoding | ||
def decodelist(l, sep=' '): | ||||
Peter Arrenbrecht
|
r13722 | if l: | ||
Augie Fackler
|
r34730 | return [bin(v) for v in l.split(sep)] | ||
Peter Arrenbrecht
|
r13722 | return [] | ||
Benoit Boissinot
|
r11597 | |||
def encodelist(l, sep=' '): | ||||
Pierre-Yves David
|
r23848 | try: | ||
return sep.join(map(hex, l)) | ||||
except TypeError: | ||||
raise | ||||
Benoit Boissinot
|
r11597 | |||
Peter Arrenbrecht
|
r14622 | # batched call argument encoding | ||
def escapearg(plain): | ||||
return (plain | ||||
Augie Fackler
|
r25708 | .replace(':', ':c') | ||
.replace(',', ':o') | ||||
.replace(';', ':s') | ||||
.replace('=', ':e')) | ||||
Peter Arrenbrecht
|
r14622 | |||
def unescapearg(escaped): | ||||
return (escaped | ||||
Augie Fackler
|
r25708 | .replace(':e', '=') | ||
.replace(':s', ';') | ||||
.replace(':o', ',') | ||||
.replace(':c', ':')) | ||||
Peter Arrenbrecht
|
r14622 | |||
Gregory Szorc
|
r29733 | def encodebatchcmds(req): | ||
"""Return a ``cmds`` argument value for the ``batch`` command.""" | ||||
cmds = [] | ||||
for op, argsdict in req: | ||||
Gregory Szorc
|
r29734 | # Old servers didn't properly unescape argument names. So prevent | ||
# the sending of argument names that may not be decoded properly by | ||||
# servers. | ||||
assert all(escapearg(k) == k for k in argsdict) | ||||
Gregory Szorc
|
r29733 | args = ','.join('%s=%s' % (escapearg(k), escapearg(v)) | ||
for k, v in argsdict.iteritems()) | ||||
cmds.append('%s %s' % (op, args)) | ||||
return ';'.join(cmds) | ||||
Pierre-Yves David
|
r21646 | # mapping of options accepted by getbundle and their types | ||
# | ||||
# Meant to be extended by extensions. It is extensions responsibility to ensure | ||||
# such options are properly processed in exchange.getbundle. | ||||
# | ||||
# supported types are: | ||||
# | ||||
# :nodes: list of binary nodes | ||||
# :csv: list of comma-separated values | ||||
Pierre-Yves David
|
r25403 | # :scsv: list of comma-separated values return as set | ||
Pierre-Yves David
|
r21646 | # :plain: string with no transformation needed. | ||
gboptsmap = {'heads': 'nodes', | ||||
Boris Feld
|
r35268 | 'bookmarks': 'boolean', | ||
Pierre-Yves David
|
r21646 | 'common': 'nodes', | ||
Pierre-Yves David
|
r22353 | 'obsmarkers': 'boolean', | ||
Boris Feld
|
r34323 | 'phases': 'boolean', | ||
Pierre-Yves David
|
r25403 | 'bundlecaps': 'scsv', | ||
Pierre-Yves David
|
r21989 | 'listkeys': 'csv', | ||
Gregory Szorc
|
r26690 | 'cg': 'boolean', | ||
Boris Feld
|
r35777 | 'cbattempted': 'boolean', | ||
'stream': 'boolean', | ||||
} | ||||
Pierre-Yves David
|
r21646 | |||
Matt Mackall
|
r11586 | # client side | ||
Gregory Szorc
|
r33805 | class wirepeer(repository.legacypeer): | ||
Gregory Szorc
|
r27243 | """Client-side interface for communicating with a peer repository. | ||
Peter Arrenbrecht
|
r14622 | |||
Gregory Szorc
|
r27243 | Methods commonly call wire protocol commands of the same name. | ||
See also httppeer.py and sshpeer.py for protocol-specific | ||||
implementations of this interface. | ||||
""" | ||||
Gregory Szorc
|
r33805 | # Begin of basewirepeer interface. | ||
Peter Arrenbrecht
|
r14622 | |||
Augie Fackler
|
r28436 | def iterbatch(self): | ||
return remoteiterbatcher(self) | ||||
Peter Arrenbrecht
|
r14623 | @batchable | ||
Matt Mackall
|
r11586 | def lookup(self, key): | ||
self.requirecap('lookup', _('look up remote revision')) | ||||
Peter Arrenbrecht
|
r14623 | f = future() | ||
Augie Fackler
|
r20671 | yield {'key': encoding.fromlocal(key)}, f | ||
Peter Arrenbrecht
|
r14623 | d = f.value | ||
Matt Mackall
|
r11586 | success, data = d[:-1].split(" ", 1) | ||
if int(success): | ||||
Peter Arrenbrecht
|
r14623 | yield bin(data) | ||
Kyle Lippincott
|
r34064 | else: | ||
self._abort(error.RepoError(data)) | ||||
Matt Mackall
|
r11586 | |||
Peter Arrenbrecht
|
r14623 | @batchable | ||
Matt Mackall
|
r11586 | def heads(self): | ||
Peter Arrenbrecht
|
r14623 | f = future() | ||
yield {}, f | ||||
d = f.value | ||||
Matt Mackall
|
r11586 | try: | ||
Peter Arrenbrecht
|
r14623 | yield decodelist(d[:-1]) | ||
Matt Mackall
|
r13726 | except ValueError: | ||
Benoit Boissinot
|
r11879 | self._abort(error.ResponseError(_("unexpected response:"), d)) | ||
Matt Mackall
|
r11586 | |||
Peter Arrenbrecht
|
r14623 | @batchable | ||
Peter Arrenbrecht
|
r13723 | def known(self, nodes): | ||
Peter Arrenbrecht
|
r14623 | f = future() | ||
Augie Fackler
|
r20671 | yield {'nodes': encodelist(nodes)}, f | ||
Peter Arrenbrecht
|
r14623 | d = f.value | ||
Peter Arrenbrecht
|
r13723 | try: | ||
Mads Kiilerich
|
r22201 | yield [bool(int(b)) for b in d] | ||
Matt Mackall
|
r13726 | except ValueError: | ||
Peter Arrenbrecht
|
r13723 | self._abort(error.ResponseError(_("unexpected response:"), d)) | ||
Peter Arrenbrecht
|
r14623 | @batchable | ||
Matt Mackall
|
r11586 | def branchmap(self): | ||
Peter Arrenbrecht
|
r14623 | f = future() | ||
yield {}, f | ||||
d = f.value | ||||
Matt Mackall
|
r11586 | try: | ||
branchmap = {} | ||||
for branchpart in d.splitlines(): | ||||
Benoit Boissinot
|
r11597 | branchname, branchheads = branchpart.split(' ', 1) | ||
timeless
|
r28883 | branchname = encoding.tolocal(urlreq.unquote(branchname)) | ||
Benoit Boissinot
|
r11597 | branchheads = decodelist(branchheads) | ||
Matt Mackall
|
r11586 | branchmap[branchname] = branchheads | ||
Peter Arrenbrecht
|
r14623 | yield branchmap | ||
Matt Mackall
|
r11586 | except TypeError: | ||
self._abort(error.ResponseError(_("unexpected response:"), d)) | ||||
Gregory Szorc
|
r33805 | @batchable | ||
def listkeys(self, namespace): | ||||
if not self.capable('pushkey'): | ||||
yield {}, None | ||||
f = future() | ||||
self.ui.debug('preparing listkeys for "%s"\n' % namespace) | ||||
yield {'namespace': encoding.fromlocal(namespace)}, f | ||||
d = f.value | ||||
self.ui.debug('received listkey for "%s": %i bytes\n' | ||||
% (namespace, len(d))) | ||||
yield pushkeymod.decodekeys(d) | ||||
Matt Mackall
|
r11586 | |||
Peter Arrenbrecht
|
r14623 | @batchable | ||
Matt Mackall
|
r11586 | def pushkey(self, namespace, key, old, new): | ||
if not self.capable('pushkey'): | ||||
Peter Arrenbrecht
|
r14623 | yield False, None | ||
f = future() | ||||
Pierre-Yves David
|
r17293 | self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key)) | ||
Augie Fackler
|
r20671 | yield {'namespace': encoding.fromlocal(namespace), | ||
'key': encoding.fromlocal(key), | ||||
'old': encoding.fromlocal(old), | ||||
'new': encoding.fromlocal(new)}, f | ||||
Peter Arrenbrecht
|
r14623 | d = f.value | ||
Pierre-Yves David
|
r15652 | d, output = d.split('\n', 1) | ||
David Soria Parra
|
r13450 | try: | ||
d = bool(int(d)) | ||||
except ValueError: | ||||
raise error.ResponseError( | ||||
_('push failed (unexpected response):'), d) | ||||
Pierre-Yves David
|
r15652 | for l in output.splitlines(True): | ||
self.ui.status(_('remote: '), l) | ||||
Peter Arrenbrecht
|
r14623 | yield d | ||
Matt Mackall
|
r11586 | |||
Matt Mackall
|
r11588 | def stream_out(self): | ||
return self._callstream('stream_out') | ||||
Pierre-Yves David
|
r21646 | def getbundle(self, source, **kwargs): | ||
Augie Fackler
|
r34740 | kwargs = pycompat.byteskwargs(kwargs) | ||
Peter Arrenbrecht
|
r13741 | self.requirecap('getbundle', _('look up remote changes')) | ||
opts = {} | ||||
Pierre-Yves David
|
r25128 | bundlecaps = kwargs.get('bundlecaps') | ||
if bundlecaps is not None: | ||||
kwargs['bundlecaps'] = sorted(bundlecaps) | ||||
else: | ||||
bundlecaps = () # kwargs could have it to None | ||||
Pierre-Yves David
|
r21646 | for key, value in kwargs.iteritems(): | ||
if value is None: | ||||
continue | ||||
keytype = gboptsmap.get(key) | ||||
if keytype is None: | ||||
Augie Fackler
|
r34731 | raise error.ProgrammingError( | ||
'Unexpectedly None keytype for key %s' % key) | ||||
Pierre-Yves David
|
r21646 | elif keytype == 'nodes': | ||
value = encodelist(value) | ||||
Pierre-Yves David
|
r25403 | elif keytype in ('csv', 'scsv'): | ||
Pierre-Yves David
|
r21646 | value = ','.join(value) | ||
Pierre-Yves David
|
r21988 | elif keytype == 'boolean': | ||
Pierre-Yves David
|
r22351 | value = '%i' % bool(value) | ||
Pierre-Yves David
|
r21646 | elif keytype != 'plain': | ||
raise KeyError('unknown getbundle option type %s' | ||||
% keytype) | ||||
opts[key] = value | ||||
Augie Fackler
|
r34740 | f = self._callcompressable("getbundle", **pycompat.strkwargs(opts)) | ||
Augie Fackler
|
r25149 | if any((cap.startswith('HG2') for cap in bundlecaps)): | ||
Pierre-Yves David
|
r24641 | return bundle2.getunbundler(self.ui, f) | ||
Pierre-Yves David
|
r21069 | else: | ||
Sune Foldager
|
r22390 | return changegroupmod.cg1unpacker(f, 'UN') | ||
Peter Arrenbrecht
|
r13741 | |||
Augie Fackler
|
r29706 | def unbundle(self, cg, heads, url): | ||
Matt Mackall
|
r11592 | '''Send cg (a readable file-like object representing the | ||
changegroup to push, typically a chunkbuffer object) to the | ||||
Pierre-Yves David
|
r21075 | remote server as a bundle. | ||
When pushing a bundle10 stream, return an integer indicating the | ||||
Martin von Zweigbergk
|
r32880 | result of the push (see changegroup.apply()). | ||
Pierre-Yves David
|
r21075 | |||
Augie Fackler
|
r29706 | When pushing a bundle20 stream, return a bundle20 stream. | ||
`url` is the url the client thinks it's pushing to, which is | ||||
visible to hooks. | ||||
''' | ||||
Matt Mackall
|
r11592 | |||
Martin Geisler
|
r14419 | if heads != ['force'] and self.capable('unbundlehash'): | ||
Shuhei Takahashi
|
r13942 | heads = encodelist(['hashed', | ||
Augie Fackler
|
r29341 | hashlib.sha1(''.join(sorted(heads))).digest()]) | ||
Shuhei Takahashi
|
r13942 | else: | ||
heads = encodelist(heads) | ||||
Pierre-Yves David
|
r21075 | if util.safehasattr(cg, 'deltaheader'): | ||
# this a bundle10, do the old style call sequence | ||||
ret, output = self._callpush("unbundle", cg, heads=heads) | ||||
if ret == "": | ||||
raise error.ResponseError( | ||||
_('push failed:'), output) | ||||
try: | ||||
ret = int(ret) | ||||
except ValueError: | ||||
raise error.ResponseError( | ||||
_('push failed (unexpected response):'), ret) | ||||
Matt Mackall
|
r11592 | |||
Pierre-Yves David
|
r21075 | for l in output.splitlines(True): | ||
self.ui.status(_('remote: '), l) | ||||
else: | ||||
# bundle2 push. Send a stream, fetch a stream. | ||||
stream = self._calltwowaystream('unbundle', cg, heads=heads) | ||||
Pierre-Yves David
|
r24641 | ret = bundle2.getunbundler(self.ui, stream) | ||
Matt Mackall
|
r11592 | return ret | ||
Gregory Szorc
|
r33805 | # End of basewirepeer interface. | ||
# Begin of baselegacywirepeer interface. | ||||
def branches(self, nodes): | ||||
n = encodelist(nodes) | ||||
d = self._call("branches", nodes=n) | ||||
try: | ||||
br = [tuple(decodelist(b)) for b in d.splitlines()] | ||||
return br | ||||
except ValueError: | ||||
self._abort(error.ResponseError(_("unexpected response:"), d)) | ||||
def between(self, pairs): | ||||
batch = 8 # avoid giant requests | ||||
r = [] | ||||
for i in xrange(0, len(pairs), batch): | ||||
n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]]) | ||||
d = self._call("between", pairs=n) | ||||
try: | ||||
r.extend(l and decodelist(l) or [] for l in d.splitlines()) | ||||
except ValueError: | ||||
self._abort(error.ResponseError(_("unexpected response:"), d)) | ||||
return r | ||||
def changegroup(self, nodes, kind): | ||||
n = encodelist(nodes) | ||||
f = self._callcompressable("changegroup", roots=n) | ||||
return changegroupmod.cg1unpacker(f, 'UN') | ||||
def changegroupsubset(self, bases, heads, kind): | ||||
self.requirecap('changegroupsubset', _('look up remote changes')) | ||||
bases = encodelist(bases) | ||||
heads = encodelist(heads) | ||||
f = self._callcompressable("changegroupsubset", | ||||
bases=bases, heads=heads) | ||||
return changegroupmod.cg1unpacker(f, 'UN') | ||||
# End of baselegacywirepeer interface. | ||||
def _submitbatch(self, req): | ||||
"""run batch request <req> on the server | ||||
Returns an iterator of the raw responses from the server. | ||||
""" | ||||
rsp = self._callstream("batch", cmds=encodebatchcmds(req)) | ||||
chunk = rsp.read(1024) | ||||
work = [chunk] | ||||
while chunk: | ||||
while ';' not in chunk and chunk: | ||||
chunk = rsp.read(1024) | ||||
work.append(chunk) | ||||
merged = ''.join(work) | ||||
while ';' in merged: | ||||
one, merged = merged.split(';', 1) | ||||
yield unescapearg(one) | ||||
chunk = rsp.read(1024) | ||||
work = [merged, chunk] | ||||
yield unescapearg(''.join(work)) | ||||
def _submitone(self, op, args): | ||||
Augie Fackler
|
r34740 | return self._call(op, **pycompat.strkwargs(args)) | ||
Gregory Szorc
|
r33805 | |||
Peter Arrenbrecht
|
r14048 | def debugwireargs(self, one, two, three=None, four=None, five=None): | ||
Peter Arrenbrecht
|
r13720 | # don't pass optional arguments left at their default value | ||
opts = {} | ||||
if three is not None: | ||||
Pulkit Goyal
|
r35375 | opts[r'three'] = three | ||
Peter Arrenbrecht
|
r13720 | if four is not None: | ||
Pulkit Goyal
|
r35375 | opts[r'four'] = four | ||
Peter Arrenbrecht
|
r13720 | return self._call('debugwireargs', one=one, two=two, **opts) | ||
Pierre-Yves David
|
r20904 | def _call(self, cmd, **args): | ||
"""execute <cmd> on the server | ||||
The command is expected to return a simple string. | ||||
returns the server reply as a string.""" | ||||
raise NotImplementedError() | ||||
def _callstream(self, cmd, **args): | ||||
"""execute <cmd> on the server | ||||
Augie Fackler
|
r28435 | The command is expected to return a stream. Note that if the | ||
command doesn't return a stream, _callstream behaves | ||||
differently for ssh and http peers. | ||||
Pierre-Yves David
|
r20904 | |||
Augie Fackler
|
r28435 | returns the server reply as a file like object. | ||
""" | ||||
Pierre-Yves David
|
r20904 | raise NotImplementedError() | ||
Pierre-Yves David
|
r20905 | def _callcompressable(self, cmd, **args): | ||
"""execute <cmd> on the server | ||||
The command is expected to return a stream. | ||||
Mads Kiilerich
|
r21024 | The stream may have been compressed in some implementations. This | ||
Pierre-Yves David
|
r20905 | function takes care of the decompression. This is the only difference | ||
with _callstream. | ||||
returns the server reply as a file like object. | ||||
""" | ||||
raise NotImplementedError() | ||||
Pierre-Yves David
|
r20904 | def _callpush(self, cmd, fp, **args): | ||
"""execute a <cmd> on server | ||||
The command is expected to be related to a push. Push has a special | ||||
return method. | ||||
returns the server reply as a (ret, output) tuple. ret is either | ||||
empty (error) or a stringified int. | ||||
""" | ||||
raise NotImplementedError() | ||||
Pierre-Yves David
|
r21072 | def _calltwowaystream(self, cmd, fp, **args): | ||
"""execute <cmd> on server | ||||
The command will send a stream to the server and get a stream in reply. | ||||
""" | ||||
raise NotImplementedError() | ||||
Pierre-Yves David
|
r20904 | def _abort(self, exception): | ||
"""clearly abort the wire protocol connection and raise the exception | ||||
""" | ||||
raise NotImplementedError() | ||||
Matt Mackall
|
r11586 | # server side | ||
Pierre-Yves David
|
r20902 | # wire protocol command can either return a string or one of these classes. | ||
Andrew Pritchard
|
r15017 | |||
Gregory Szorc
|
r29590 | def getdispatchrepo(repo, proto, command): | ||
"""Obtain the repo used for processing wire protocol commands. | ||||
The intent of this function is to serve as a monkeypatch point for | ||||
extensions that need commands to operate on different repo views under | ||||
specialized circumstances. | ||||
""" | ||||
return repo.filtered('served') | ||||
Matt Mackall
|
r11581 | def dispatch(repo, proto, command): | ||
Gregory Szorc
|
r29590 | repo = getdispatchrepo(repo, proto, command) | ||
Matt Mackall
|
r11581 | func, spec = commands[command] | ||
args = proto.getargs(spec) | ||||
Dirkjan Ochtman
|
r11625 | return func(repo, proto, *args) | ||
Matt Mackall
|
r11581 | |||
Peter Arrenbrecht
|
r13721 | def options(cmd, keys, others): | ||
opts = {} | ||||
for k in keys: | ||||
if k in others: | ||||
opts[k] = others[k] | ||||
del others[k] | ||||
if others: | ||||
Yuya Nishihara
|
r30473 | util.stderr.write("warning: %s ignored unexpected arguments %s\n" | ||
% (cmd, ",".join(others))) | ||||
Peter Arrenbrecht
|
r13721 | return opts | ||
Gregory Szorc
|
r27633 | def bundle1allowed(repo, action): | ||
"""Whether a bundle1 operation is allowed from the server. | ||||
Priority is: | ||||
1. server.bundle1gd.<action> (if generaldelta active) | ||||
2. server.bundle1.<action> | ||||
3. server.bundle1gd (if generaldelta active) | ||||
4. server.bundle1 | ||||
""" | ||||
ui = repo.ui | ||||
gd = 'generaldelta' in repo.requirements | ||||
if gd: | ||||
Boris Feld
|
r34614 | v = ui.configbool('server', 'bundle1gd.%s' % action) | ||
Gregory Szorc
|
r27633 | if v is not None: | ||
return v | ||||
Boris Feld
|
r34614 | v = ui.configbool('server', 'bundle1.%s' % action) | ||
Gregory Szorc
|
r27246 | if v is not None: | ||
return v | ||||
Gregory Szorc
|
r27633 | if gd: | ||
r33217 | v = ui.configbool('server', 'bundle1gd') | |||
Gregory Szorc
|
r27633 | if v is not None: | ||
return v | ||||
r33216 | return ui.configbool('server', 'bundle1') | |||
Gregory Szorc
|
r27246 | |||
Gregory Szorc
|
r36088 | def supportedcompengines(ui, role): | ||
Gregory Szorc
|
r30762 | """Obtain the list of supported compression engines for a request.""" | ||
assert role in (util.CLIENTROLE, util.SERVERROLE) | ||||
compengines = util.compengines.supportedwireengines(role) | ||||
# Allow config to override default list and ordering. | ||||
if role == util.SERVERROLE: | ||||
configengines = ui.configlist('server', 'compressionengines') | ||||
config = 'server.compressionengines' | ||||
else: | ||||
# This is currently implemented mainly to facilitate testing. In most | ||||
# cases, the server should be in charge of choosing a compression engine | ||||
# because a server has the most to lose from a sub-optimal choice. (e.g. | ||||
# CPU DoS due to an expensive engine or a network DoS due to poor | ||||
# compression ratio). | ||||
configengines = ui.configlist('experimental', | ||||
'clientcompressionengines') | ||||
config = 'experimental.clientcompressionengines' | ||||
# No explicit config. Filter out the ones that aren't supposed to be | ||||
# advertised and return default ordering. | ||||
if not configengines: | ||||
attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority' | ||||
return [e for e in compengines | ||||
if getattr(e.wireprotosupport(), attr) > 0] | ||||
# If compression engines are listed in the config, assume there is a good | ||||
# reason for it (like server operators wanting to achieve specific | ||||
# performance characteristics). So fail fast if the config references | ||||
# unusable compression engines. | ||||
validnames = set(e.name() for e in compengines) | ||||
invalidnames = set(e for e in configengines if e not in validnames) | ||||
if invalidnames: | ||||
raise error.Abort(_('invalid compression engine defined in %s: %s') % | ||||
(config, ', '.join(sorted(invalidnames)))) | ||||
compengines = [e for e in compengines if e.name() in configengines] | ||||
compengines = sorted(compengines, | ||||
key=lambda e: configengines.index(e.name())) | ||||
if not compengines: | ||||
raise error.Abort(_('%s config option does not specify any known ' | ||||
'compression engines') % config, | ||||
hint=_('usable compression engines: %s') % | ||||
', '.sorted(validnames)) | ||||
return compengines | ||||
Gregory Szorc
|
r35999 | class commandentry(object): | ||
"""Represents a declared wire protocol command.""" | ||||
Gregory Szorc
|
r36627 | def __init__(self, func, args='', transports=None): | ||
Gregory Szorc
|
r35999 | self.func = func | ||
self.args = args | ||||
Gregory Szorc
|
r36627 | self.transports = transports or set() | ||
Gregory Szorc
|
r35999 | |||
def _merge(self, func, args): | ||||
"""Merge this instance with an incoming 2-tuple. | ||||
This is called when a caller using the old 2-tuple API attempts | ||||
to replace an instance. The incoming values are merged with | ||||
data not captured by the 2-tuple and a new instance containing | ||||
the union of the two objects is returned. | ||||
""" | ||||
Gregory Szorc
|
r36627 | return commandentry(func, args=args, transports=set(self.transports)) | ||
Gregory Szorc
|
r35999 | |||
# Old code treats instances as 2-tuples. So expose that interface. | ||||
def __iter__(self): | ||||
yield self.func | ||||
yield self.args | ||||
def __getitem__(self, i): | ||||
if i == 0: | ||||
return self.func | ||||
elif i == 1: | ||||
return self.args | ||||
else: | ||||
raise IndexError('can only access elements 0 and 1') | ||||
class commanddict(dict): | ||||
"""Container for registered wire protocol commands. | ||||
It behaves like a dict. But __setitem__ is overwritten to allow silent | ||||
coercion of values from 2-tuples for API compatibility. | ||||
""" | ||||
def __setitem__(self, k, v): | ||||
if isinstance(v, commandentry): | ||||
pass | ||||
# Cast 2-tuples to commandentry instances. | ||||
elif isinstance(v, tuple): | ||||
if len(v) != 2: | ||||
raise ValueError('command tuples must have exactly 2 elements') | ||||
# It is common for extensions to wrap wire protocol commands via | ||||
# e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers | ||||
# doing this aren't aware of the new API that uses objects to store | ||||
# command entries, we automatically merge old state with new. | ||||
if k in self: | ||||
v = self[k]._merge(v[0], v[1]) | ||||
else: | ||||
Gregory Szorc
|
r36627 | # Use default values from @wireprotocommand. | ||
v = commandentry(v[0], args=v[1], | ||||
transports=set(wireprototypes.TRANSPORTS)) | ||||
Gregory Szorc
|
r35999 | else: | ||
raise ValueError('command entries must be commandentry instances ' | ||||
'or 2-tuples') | ||||
return super(commanddict, self).__setitem__(k, v) | ||||
Gregory Szorc
|
r36000 | def commandavailable(self, command, proto): | ||
"""Determine if a command is available for the requested protocol.""" | ||||
Gregory Szorc
|
r36627 | assert proto.name in wireprototypes.TRANSPORTS | ||
entry = self.get(command) | ||||
if not entry: | ||||
return False | ||||
if proto.name not in entry.transports: | ||||
return False | ||||
return True | ||||
# Constants specifying which transports a wire protocol command should be | ||||
# available on. For use with @wireprotocommand. | ||||
POLICY_ALL = 'all' | ||||
POLICY_V1_ONLY = 'v1-only' | ||||
POLICY_V2_ONLY = 'v2-only' | ||||
Gregory Szorc
|
r36000 | |||
Gregory Szorc
|
r35999 | commands = commanddict() | ||
Pierre-Yves David
|
r20906 | |||
Gregory Szorc
|
r36627 | def wireprotocommand(name, args='', transportpolicy=POLICY_ALL): | ||
Gregory Szorc
|
r35998 | """Decorator to declare a wire protocol command. | ||
``name`` is the name of the wire protocol command being provided. | ||||
``args`` is a space-delimited list of named arguments that the command | ||||
accepts. ``*`` is a special value that says to accept all arguments. | ||||
Gregory Szorc
|
r36627 | |||
``transportpolicy`` is a POLICY_* constant denoting which transports | ||||
this wire protocol command should be exposed to. By default, commands | ||||
are exposed to all wire protocol transports. | ||||
Gregory Szorc
|
r35998 | """ | ||
Gregory Szorc
|
r36627 | if transportpolicy == POLICY_ALL: | ||
transports = set(wireprototypes.TRANSPORTS) | ||||
elif transportpolicy == POLICY_V1_ONLY: | ||||
transports = {k for k, v in wireprototypes.TRANSPORTS.items() | ||||
if v['version'] == 1} | ||||
elif transportpolicy == POLICY_V2_ONLY: | ||||
transports = {k for k, v in wireprototypes.TRANSPORTS.items() | ||||
if v['version'] == 2} | ||||
else: | ||||
raise error.Abort(_('invalid transport policy value: %s') % | ||||
transportpolicy) | ||||
Pierre-Yves David
|
r20906 | def register(func): | ||
Gregory Szorc
|
r36627 | commands[name] = commandentry(func, args=args, transports=transports) | ||
Pierre-Yves David
|
r20906 | return func | ||
return register | ||||
Pierre-Yves David
|
r20907 | @wireprotocommand('batch', 'cmds *') | ||
Peter Arrenbrecht
|
r14622 | def batch(repo, proto, cmds, others): | ||
Kevin Bullock
|
r18382 | repo = repo.filtered("served") | ||
Peter Arrenbrecht
|
r14622 | res = [] | ||
for pair in cmds.split(';'): | ||||
op, args = pair.split(' ', 1) | ||||
vals = {} | ||||
for a in args.split(','): | ||||
if a: | ||||
n, v = a.split('=') | ||||
Gregory Szorc
|
r29734 | vals[unescapearg(n)] = unescapearg(v) | ||
Peter Arrenbrecht
|
r14622 | func, spec = commands[op] | ||
if spec: | ||||
keys = spec.split() | ||||
data = {} | ||||
for k in keys: | ||||
if k == '*': | ||||
star = {} | ||||
for key in vals.keys(): | ||||
if key not in keys: | ||||
star[key] = vals[key] | ||||
data['*'] = star | ||||
else: | ||||
data[k] = vals[k] | ||||
result = func(repo, proto, *[data[k] for k in keys]) | ||||
else: | ||||
result = func(repo, proto) | ||||
Andrew Pritchard
|
r15017 | if isinstance(result, ooberror): | ||
return result | ||||
Gregory Szorc
|
r36091 | |||
# For now, all batchable commands must return bytesresponse or | ||||
# raw bytes (for backwards compatibility). | ||||
assert isinstance(result, (bytesresponse, bytes)) | ||||
if isinstance(result, bytesresponse): | ||||
result = result.data | ||||
Peter Arrenbrecht
|
r14622 | res.append(escapearg(result)) | ||
Gregory Szorc
|
r36091 | |||
return bytesresponse(';'.join(res)) | ||||
Peter Arrenbrecht
|
r14622 | |||
Gregory Szorc
|
r36633 | @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY) | ||
Matt Mackall
|
r11583 | def between(repo, proto, pairs): | ||
Benoit Boissinot
|
r11597 | pairs = [decodelist(p, '-') for p in pairs.split(" ")] | ||
Matt Mackall
|
r11581 | r = [] | ||
for b in repo.between(pairs): | ||||
Benoit Boissinot
|
r11597 | r.append(encodelist(b) + "\n") | ||
Gregory Szorc
|
r36091 | |||
return bytesresponse(''.join(r)) | ||||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20909 | @wireprotocommand('branchmap') | ||
Matt Mackall
|
r11583 | def branchmap(repo, proto): | ||
Pierre-Yves David
|
r18281 | branchmap = repo.branchmap() | ||
Matt Mackall
|
r11581 | heads = [] | ||
for branch, nodes in branchmap.iteritems(): | ||||
timeless
|
r28883 | branchname = urlreq.quote(encoding.fromlocal(branch)) | ||
Benoit Boissinot
|
r11597 | branchnodes = encodelist(nodes) | ||
heads.append('%s %s' % (branchname, branchnodes)) | ||||
Gregory Szorc
|
r36091 | |||
return bytesresponse('\n'.join(heads)) | ||||
Matt Mackall
|
r11581 | |||
Gregory Szorc
|
r36629 | @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY) | ||
Matt Mackall
|
r11583 | def branches(repo, proto, nodes): | ||
Benoit Boissinot
|
r11597 | nodes = decodelist(nodes) | ||
Matt Mackall
|
r11581 | r = [] | ||
for b in repo.branches(nodes): | ||||
Benoit Boissinot
|
r11597 | r.append(encodelist(b) + "\n") | ||
Gregory Szorc
|
r36091 | |||
return bytesresponse(''.join(r)) | ||||
Matt Mackall
|
r11581 | |||
Gregory Szorc
|
r26857 | @wireprotocommand('clonebundles', '') | ||
def clonebundles(repo, proto): | ||||
"""Server command for returning info for available bundles to seed clones. | ||||
Clients will parse this response and determine what bundle to fetch. | ||||
Extensions may wrap this command to filter or dynamically emit data | ||||
depending on the request. e.g. you could advertise URLs for the closest | ||||
data center given the client's IP address. | ||||
""" | ||||
Gregory Szorc
|
r36091 | return bytesresponse(repo.vfs.tryread('clonebundles.manifest')) | ||
Pierre-Yves David
|
r20774 | |||
Gregory Szorc
|
r36630 | wireprotocaps = ['lookup', 'branchmap', 'pushkey', | ||
Pierre-Yves David
|
r20774 | 'known', 'getbundle', 'unbundlehash', 'batch'] | ||
Pierre-Yves David
|
r20775 | |||
def _capabilities(repo, proto): | ||||
"""return a list of capabilities for a repo | ||||
This function exists to allow extensions to easily wrap capabilities | ||||
computation | ||||
- returns a lists: easy to alter | ||||
- change done here will be propagated to both `capabilities` and `hello` | ||||
Mads Kiilerich
|
r21024 | command without any other action needed. | ||
Pierre-Yves David
|
r20775 | """ | ||
Pierre-Yves David
|
r20774 | # copy to prevent modification of the global list | ||
caps = list(wireprotocaps) | ||||
Gregory Szorc
|
r36630 | |||
# Command of same name as capability isn't exposed to version 1 of | ||||
# transports. So conditionally add it. | ||||
if commands.commandavailable('changegroupsubset', proto): | ||||
caps.append('changegroupsubset') | ||||
Gregory Szorc
|
r32744 | if streamclone.allowservergeneration(repo): | ||
r33222 | if repo.ui.configbool('server', 'preferuncompressed'): | |||
Benoit Allard
|
r16361 | caps.append('stream-preferred') | ||
Sune Foldager
|
r12296 | requiredformats = repo.requirements & repo.supportedformats | ||
# if our local revlogs are just revlogv1, add 'stream' cap | ||||
Martin von Zweigbergk
|
r32291 | if not requiredformats - {'revlogv1'}: | ||
Sune Foldager
|
r12296 | caps.append('stream') | ||
# otherwise, add 'streamreqs' detailing our local revlog format | ||||
else: | ||||
Pierre-Yves David
|
r26911 | caps.append('streamreqs=%s' % ','.join(sorted(requiredformats))) | ||
Jun Wu
|
r33499 | if repo.ui.configbool('experimental', 'bundle2-advertise'): | ||
Gregory Szorc
|
r35801 | capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server')) | ||
timeless
|
r28883 | caps.append('bundle2=' + urlreq.quote(capsblob)) | ||
Martin von Zweigbergk
|
r28666 | caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority)) | ||
Gregory Szorc
|
r30563 | |||
Gregory Szorc
|
r36631 | return proto.addcapabilities(repo, caps) | ||
Pierre-Yves David
|
r20775 | |||
Mads Kiilerich
|
r21024 | # If you are writing an extension and consider wrapping this function. Wrap | ||
Pierre-Yves David
|
r20775 | # `_capabilities` instead. | ||
Pierre-Yves David
|
r20911 | @wireprotocommand('capabilities') | ||
Pierre-Yves David
|
r20775 | def capabilities(repo, proto): | ||
Gregory Szorc
|
r36091 | return bytesresponse(' '.join(_capabilities(repo, proto))) | ||
Matt Mackall
|
r11594 | |||
Gregory Szorc
|
r36629 | @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY) | ||
Matt Mackall
|
r11584 | def changegroup(repo, proto, roots): | ||
Benoit Boissinot
|
r11597 | nodes = decodelist(roots) | ||
Durham Goode
|
r34102 | outgoing = discovery.outgoing(repo, missingroots=nodes, | ||
missingheads=repo.heads()) | ||||
cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve') | ||||
Gregory Szorc
|
r35723 | gen = iter(lambda: cg.read(32768), '') | ||
Joerg Sonnenberger
|
r35768 | return streamres(gen=gen) | ||
Matt Mackall
|
r11584 | |||
Gregory Szorc
|
r36629 | @wireprotocommand('changegroupsubset', 'bases heads', | ||
transportpolicy=POLICY_V1_ONLY) | ||||
Matt Mackall
|
r11584 | def changegroupsubset(repo, proto, bases, heads): | ||
Benoit Boissinot
|
r11597 | bases = decodelist(bases) | ||
heads = decodelist(heads) | ||||
Durham Goode
|
r34099 | outgoing = discovery.outgoing(repo, missingroots=bases, | ||
missingheads=heads) | ||||
cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve') | ||||
Gregory Szorc
|
r35723 | gen = iter(lambda: cg.read(32768), '') | ||
Joerg Sonnenberger
|
r35768 | return streamres(gen=gen) | ||
Matt Mackall
|
r11584 | |||
Pierre-Yves David
|
r20914 | @wireprotocommand('debugwireargs', 'one two *') | ||
Peter Arrenbrecht
|
r13721 | def debugwireargs(repo, proto, one, two, others): | ||
# only accept optional args from the known set | ||||
opts = options('debugwireargs', ['three', 'four'], others) | ||||
Gregory Szorc
|
r36091 | return bytesresponse(repo.debugwireargs(one, two, | ||
**pycompat.strkwargs(opts))) | ||||
Peter Arrenbrecht
|
r13720 | |||
Pierre-Yves David
|
r20915 | @wireprotocommand('getbundle', '*') | ||
Peter Arrenbrecht
|
r13741 | def getbundle(repo, proto, others): | ||
Pierre-Yves David
|
r21646 | opts = options('getbundle', gboptsmap.keys(), others) | ||
Peter Arrenbrecht
|
r13741 | for k, v in opts.iteritems(): | ||
Pierre-Yves David
|
r21646 | keytype = gboptsmap[k] | ||
if keytype == 'nodes': | ||||
Benoit Boissinot
|
r19201 | opts[k] = decodelist(v) | ||
Pierre-Yves David
|
r21646 | elif keytype == 'csv': | ||
Pierre-Yves David
|
r25403 | opts[k] = list(v.split(',')) | ||
elif keytype == 'scsv': | ||||
Benoit Boissinot
|
r19201 | opts[k] = set(v.split(',')) | ||
Pierre-Yves David
|
r21988 | elif keytype == 'boolean': | ||
Gregory Szorc
|
r26686 | # Client should serialize False as '0', which is a non-empty string | ||
# so it evaluates as a True bool. | ||||
if v == '0': | ||||
opts[k] = False | ||||
else: | ||||
opts[k] = bool(v) | ||||
Pierre-Yves David
|
r21646 | elif keytype != 'plain': | ||
raise KeyError('unknown getbundle option type %s' | ||||
% keytype) | ||||
Gregory Szorc
|
r27246 | |||
Gregory Szorc
|
r27633 | if not bundle1allowed(repo, 'pull'): | ||
Gregory Szorc
|
r27246 | if not exchange.bundle2requested(opts.get('bundlecaps')): | ||
Gregory Szorc
|
r36241 | if proto.name == 'http-v1': | ||
Pierre-Yves David
|
r30912 | return ooberror(bundle2required) | ||
raise error.Abort(bundle2requiredmain, | ||||
hint=bundle2requiredhint) | ||||
Gregory Szorc
|
r27246 | |||
Gregory Szorc
|
r35805 | prefercompressed = True | ||
Gregory Szorc
|
r35800 | |||
Pierre-Yves David
|
r30914 | try: | ||
r33220 | if repo.ui.configbool('server', 'disablefullbundle'): | |||
Siddharth Agarwal
|
r32260 | # Check to see if this is a full clone. | ||
clheads = set(repo.changelog.heads()) | ||||
Boris Feld
|
r35778 | changegroup = opts.get('cg', True) | ||
Siddharth Agarwal
|
r32260 | heads = set(opts.get('heads', set())) | ||
common = set(opts.get('common', set())) | ||||
common.discard(nullid) | ||||
Boris Feld
|
r35778 | if changegroup and not common and clheads == heads: | ||
Siddharth Agarwal
|
r32260 | raise error.Abort( | ||
_('server has pull-based clones disabled'), | ||||
hint=_('remove --pull if specified or upgrade Mercurial')) | ||||
Gregory Szorc
|
r35803 | info, chunks = exchange.getbundlechunks(repo, 'serve', | ||
**pycompat.strkwargs(opts)) | ||||
Gregory Szorc
|
r35805 | prefercompressed = info.get('prefercompressed', True) | ||
Pierre-Yves David
|
r30914 | except error.Abort as exc: | ||
# cleanly forward Abort error to the client | ||||
if not exchange.bundle2requested(opts.get('bundlecaps')): | ||||
Gregory Szorc
|
r36241 | if proto.name == 'http-v1': | ||
Augie Fackler
|
r36272 | return ooberror(pycompat.bytestr(exc) + '\n') | ||
Pierre-Yves David
|
r30914 | raise # cannot do better for bundle1 + ssh | ||
# bundle2 request expect a bundle2 reply | ||||
bundler = bundle2.bundle20(repo.ui) | ||||
Augie Fackler
|
r36272 | manargs = [('message', pycompat.bytestr(exc))] | ||
Pierre-Yves David
|
r30914 | advargs = [] | ||
if exc.hint is not None: | ||||
advargs.append(('hint', exc.hint)) | ||||
bundler.addpart(bundle2.bundlepart('error:abort', | ||||
manargs, advargs)) | ||||
Gregory Szorc
|
r35800 | chunks = bundler.getchunks() | ||
Gregory Szorc
|
r35805 | prefercompressed = False | ||
Gregory Szorc
|
r35800 | |||
Gregory Szorc
|
r35805 | return streamres(gen=chunks, prefer_uncompressed=not prefercompressed) | ||
Peter Arrenbrecht
|
r13741 | |||
Pierre-Yves David
|
r20916 | @wireprotocommand('heads') | ||
Matt Mackall
|
r11583 | def heads(repo, proto): | ||
Pierre-Yves David
|
r18281 | h = repo.heads() | ||
Gregory Szorc
|
r36091 | return bytesresponse(encodelist(h) + '\n') | ||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20917 | @wireprotocommand('hello') | ||
Matt Mackall
|
r11594 | def hello(repo, proto): | ||
Gregory Szorc
|
r36239 | """Called as part of SSH handshake to obtain server info. | ||
Returns a list of lines describing interesting things about the | ||||
server, in an RFC822-like format. | ||||
Matt Mackall
|
r11594 | |||
Gregory Szorc
|
r36239 | Currently, the only one defined is ``capabilities``, which consists of a | ||
line of space separated tokens describing server abilities: | ||||
capabilities: <token0> <token1> <token2> | ||||
""" | ||||
Gregory Szorc
|
r36091 | caps = capabilities(repo, proto).data | ||
return bytesresponse('capabilities: %s\n' % caps) | ||||
Matt Mackall
|
r11594 | |||
Pierre-Yves David
|
r20919 | @wireprotocommand('listkeys', 'namespace') | ||
Matt Mackall
|
r11583 | def listkeys(repo, proto, namespace): | ||
Gregory Szorc
|
r36546 | d = sorted(repo.listkeys(encoding.tolocal(namespace)).items()) | ||
Gregory Szorc
|
r36091 | return bytesresponse(pushkeymod.encodekeys(d)) | ||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20920 | @wireprotocommand('lookup', 'key') | ||
Matt Mackall
|
r11583 | def lookup(repo, proto, key): | ||
Matt Mackall
|
r11581 | try: | ||
Matt Mackall
|
r15925 | k = encoding.tolocal(key) | ||
c = repo[k] | ||||
r = c.hex() | ||||
Matt Mackall
|
r11581 | success = 1 | ||
Gregory Szorc
|
r25660 | except Exception as inst: | ||
Augie Fackler
|
r36332 | r = util.forcebytestr(inst) | ||
Matt Mackall
|
r11581 | success = 0 | ||
Gregory Szorc
|
r36091 | return bytesresponse('%d %s\n' % (success, r)) | ||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20918 | @wireprotocommand('known', 'nodes *') | ||
Peter Arrenbrecht
|
r14436 | def known(repo, proto, nodes, others): | ||
Gregory Szorc
|
r36091 | v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes))) | ||
return bytesresponse(v) | ||||
Peter Arrenbrecht
|
r13723 | |||
Pierre-Yves David
|
r20921 | @wireprotocommand('pushkey', 'namespace key old new') | ||
Matt Mackall
|
r11583 | def pushkey(repo, proto, namespace, key, old, new): | ||
Matt Mackall
|
r13050 | # compatibility with pre-1.8 clients which were accidentally | ||
# sending raw binary nodes rather than utf-8-encoded hex | ||||
Yuya Nishihara
|
r31451 | if len(new) == 20 and util.escapestr(new) != new: | ||
Matt Mackall
|
r13050 | # looks like it could be a binary node | ||
try: | ||||
Alexander Solovyov
|
r14064 | new.decode('utf-8') | ||
Matt Mackall
|
r13050 | new = encoding.tolocal(new) # but cleanly decodes as UTF-8 | ||
except UnicodeDecodeError: | ||||
pass # binary, leave unmodified | ||||
else: | ||||
new = encoding.tolocal(new) # normal path | ||||
Gregory Szorc
|
r36083 | with proto.mayberedirectstdio() as output: | ||
Gregory Szorc
|
r35997 | r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key), | ||
encoding.tolocal(old), new) or False | ||||
Wagner Bruna
|
r17793 | |||
Gregory Szorc
|
r36083 | output = output.getvalue() if output else '' | ||
Augie Fackler
|
r36449 | return bytesresponse('%d\n%s' % (int(r), output)) | ||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20922 | @wireprotocommand('stream_out') | ||
Matt Mackall
|
r11585 | def stream(repo, proto): | ||
Dirkjan Ochtman
|
r11627 | '''If the server supports streaming clone, it advertises the "stream" | ||
capability with a value representing the version and flags of the repo | ||||
it is serving. Client checks to see if it understands the format. | ||||
''' | ||||
Joerg Sonnenberger
|
r35768 | return streamres_legacy(streamclone.generatev1wireproto(repo)) | ||
Matt Mackall
|
r11585 | |||
Pierre-Yves David
|
r20923 | @wireprotocommand('unbundle', 'heads') | ||
Matt Mackall
|
r11593 | def unbundle(repo, proto, heads): | ||
Benoit Boissinot
|
r11597 | their_heads = decodelist(heads) | ||
Matt Mackall
|
r11593 | |||
Gregory Szorc
|
r36084 | with proto.mayberedirectstdio() as output: | ||
Matt Mackall
|
r11593 | try: | ||
Gregory Szorc
|
r36084 | exchange.check_heads(repo, their_heads, 'preparing changes') | ||
Gregory Szorc
|
r27246 | |||
Gregory Szorc
|
r36084 | # write bundle data to temporary file because it can be big | ||
fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') | ||||
fp = os.fdopen(fd, pycompat.sysstr('wb+')) | ||||
r = 0 | ||||
Pierre-Yves David
|
r24796 | try: | ||
Gregory Szorc
|
r36087 | proto.forwardpayload(fp) | ||
Gregory Szorc
|
r36084 | fp.seek(0) | ||
gen = exchange.readbundle(repo.ui, fp, None) | ||||
if (isinstance(gen, changegroupmod.cg1unpacker) | ||||
and not bundle1allowed(repo, 'push')): | ||||
Gregory Szorc
|
r36241 | if proto.name == 'http-v1': | ||
Gregory Szorc
|
r36084 | # need to special case http because stderr do not get to | ||
# the http client on failed push so we need to abuse | ||||
# some other error type to make sure the message get to | ||||
# the user. | ||||
return ooberror(bundle2required) | ||||
raise error.Abort(bundle2requiredmain, | ||||
hint=bundle2requiredhint) | ||||
Pierre-Yves David
|
r24796 | |||
Gregory Szorc
|
r36084 | r = exchange.unbundle(repo, gen, their_heads, 'serve', | ||
Gregory Szorc
|
r36086 | proto.client()) | ||
Gregory Szorc
|
r36084 | if util.safehasattr(r, 'addpart'): | ||
# The return looks streamable, we are in the bundle2 case | ||||
# and should return a stream. | ||||
return streamres_legacy(gen=r.getchunks()) | ||||
return pushres(r, output.getvalue() if output else '') | ||||
finally: | ||||
fp.close() | ||||
os.unlink(tempname) | ||||
except (error.BundleValueError, error.Abort, error.PushRaced) as exc: | ||||
# handle non-bundle2 case first | ||||
if not getattr(exc, 'duringunbundle2', False): | ||||
try: | ||||
Pierre-Yves David
|
r25493 | raise | ||
Gregory Szorc
|
r36084 | except error.Abort: | ||
# The old code we moved used util.stderr directly. | ||||
# We did not change it to minimise code change. | ||||
# This need to be moved to something proper. | ||||
# Feel free to do it. | ||||
util.stderr.write("abort: %s\n" % exc) | ||||
if exc.hint is not None: | ||||
util.stderr.write("(%s)\n" % exc.hint) | ||||
return pushres(0, output.getvalue() if output else '') | ||||
except error.PushRaced: | ||||
Pulkit Goyal
|
r36736 | return pusherr(pycompat.bytestr(exc), | ||
Gregory Szorc
|
r36084 | output.getvalue() if output else '') | ||
bundler = bundle2.bundle20(repo.ui) | ||||
for out in getattr(exc, '_bundle2salvagedoutput', ()): | ||||
bundler.addpart(out) | ||||
try: | ||||
try: | ||||
raise | ||||
except error.PushkeyFailed as exc: | ||||
# check client caps | ||||
remotecaps = getattr(exc, '_replycaps', None) | ||||
if (remotecaps is not None | ||||
and 'pushkey' not in remotecaps.get('error', ())): | ||||
# no support remote side, fallback to Abort handler. | ||||
raise | ||||
part = bundler.newpart('error:pushkey') | ||||
part.addparam('in-reply-to', exc.partid) | ||||
if exc.namespace is not None: | ||||
part.addparam('namespace', exc.namespace, | ||||
mandatory=False) | ||||
if exc.key is not None: | ||||
part.addparam('key', exc.key, mandatory=False) | ||||
if exc.new is not None: | ||||
part.addparam('new', exc.new, mandatory=False) | ||||
if exc.old is not None: | ||||
part.addparam('old', exc.old, mandatory=False) | ||||
if exc.ret is not None: | ||||
part.addparam('ret', exc.ret, mandatory=False) | ||||
except error.BundleValueError as exc: | ||||
errpart = bundler.newpart('error:unsupportedcontent') | ||||
if exc.parttype is not None: | ||||
errpart.addparam('parttype', exc.parttype) | ||||
if exc.params: | ||||
errpart.addparam('params', '\0'.join(exc.params)) | ||||
except error.Abort as exc: | ||||
Augie Fackler
|
r36332 | manargs = [('message', util.forcebytestr(exc))] | ||
Gregory Szorc
|
r36084 | advargs = [] | ||
if exc.hint is not None: | ||||
advargs.append(('hint', exc.hint)) | ||||
bundler.addpart(bundle2.bundlepart('error:abort', | ||||
manargs, advargs)) | ||||
except error.PushRaced as exc: | ||||
Augie Fackler
|
r36332 | bundler.newpart('error:pushraced', | ||
[('message', util.forcebytestr(exc))]) | ||||
Gregory Szorc
|
r36084 | return streamres_legacy(gen=bundler.getchunks()) | ||