wireproto.py
869 lines
| 29.0 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. | ||||
Benoit Boissinot
|
r11879 | import urllib, tempfile, os, sys | ||
Matt Mackall
|
r11581 | from i18n import _ | ||
from node import bin, hex | ||||
Pierre-Yves David
|
r21651 | import changegroup as changegroupmod, bundle2, pushkey as pushkeymod | ||
Pierre-Yves David
|
r20967 | import peer, error, encoding, util, store, exchange | ||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20903 | |||
class abstractserverproto(object): | ||||
"""abstract class that summarizes the protocol API | ||||
Used as reference and documentation. | ||||
""" | ||||
def getargs(self, args): | ||||
"""return the value for arguments in <args> | ||||
returns a list of values (same order as <args>)""" | ||||
raise NotImplementedError() | ||||
def getfile(self, fp): | ||||
"""write the whole content of a file into a file like object | ||||
The file is in the form:: | ||||
(<chunk-size>\n<chunk>)+0\n | ||||
chunk size is the ascii version of the int. | ||||
""" | ||||
raise NotImplementedError() | ||||
def redirect(self): | ||||
"""may setup interception for stdout and stderr | ||||
See also the `restore` method.""" | ||||
raise NotImplementedError() | ||||
# If the `redirect` function does install interception, the `restore` | ||||
# function MUST be defined. If interception is not used, this function | ||||
# MUST NOT be defined. | ||||
# | ||||
# left commented here on purpose | ||||
# | ||||
#def restore(self): | ||||
# """reinstall previous stdout and stderr and return intercepted stdout | ||||
# """ | ||||
# raise NotImplementedError() | ||||
def groupchunks(self, cg): | ||||
"""return 4096 chunks from a changegroup object | ||||
Some protocols may have compressed the contents.""" | ||||
raise NotImplementedError() | ||||
Peter Arrenbrecht
|
r14621 | # abstract batching support | ||
class future(object): | ||||
'''placeholder for a value to be set later''' | ||||
def set(self, value): | ||||
Augie Fackler
|
r14970 | if util.safehasattr(self, 'value'): | ||
Peter Arrenbrecht
|
r14621 | raise error.RepoError("future is already set") | ||
self.value = value | ||||
class batcher(object): | ||||
'''base class for batches of commands submittable in a single request | ||||
Brodie Rao
|
r16683 | All methods invoked on instances of this class are simply queued and | ||
return a a future for the result. Once you call submit(), all the queued | ||||
calls are performed and the results set in their respective futures. | ||||
Peter Arrenbrecht
|
r14621 | ''' | ||
def __init__(self): | ||||
self.calls = [] | ||||
def __getattr__(self, name): | ||||
def call(*args, **opts): | ||||
resref = future() | ||||
self.calls.append((name, args, opts, resref,)) | ||||
return resref | ||||
return call | ||||
def submit(self): | ||||
pass | ||||
class localbatch(batcher): | ||||
'''performs the queued calls directly''' | ||||
def __init__(self, local): | ||||
batcher.__init__(self) | ||||
self.local = local | ||||
def submit(self): | ||||
for name, args, opts, resref in self.calls: | ||||
resref.set(getattr(self.local, name)(*args, **opts)) | ||||
class remotebatch(batcher): | ||||
'''batches the queued calls; uses as few roundtrips as possible''' | ||||
def __init__(self, remote): | ||||
Brodie Rao
|
r16683 | '''remote must support _submitbatch(encbatch) and | ||
_submitone(op, encargs)''' | ||||
Peter Arrenbrecht
|
r14621 | batcher.__init__(self) | ||
self.remote = remote | ||||
def submit(self): | ||||
req, rsp = [], [] | ||||
for name, args, opts, resref in self.calls: | ||||
mtd = getattr(self.remote, name) | ||||
Augie Fackler
|
r14970 | batchablefn = getattr(mtd, 'batchable', None) | ||
if batchablefn is not None: | ||||
batchable = batchablefn(mtd.im_self, *args, **opts) | ||||
Peter Arrenbrecht
|
r14621 | encargsorres, encresref = batchable.next() | ||
if encresref: | ||||
req.append((name, encargsorres,)) | ||||
rsp.append((batchable, encresref, resref,)) | ||||
else: | ||||
resref.set(encargsorres) | ||||
else: | ||||
if req: | ||||
self._submitreq(req, rsp) | ||||
req, rsp = [], [] | ||||
resref.set(mtd(*args, **opts)) | ||||
if req: | ||||
self._submitreq(req, rsp) | ||||
def _submitreq(self, req, rsp): | ||||
encresults = self.remote._submitbatch(req) | ||||
for encres, r in zip(encresults, rsp): | ||||
batchable, encresref, resref = r | ||||
encresref.set(encres) | ||||
resref.set(batchable.next()) | ||||
def batchable(f): | ||||
'''annotation for batchable methods | ||||
Such methods must implement a coroutine as follows: | ||||
@batchable | ||||
def sample(self, one, two=None): | ||||
# Handle locally computable results first: | ||||
if not one: | ||||
yield "a local result", None | ||||
# Build list of encoded arguments suitable for your wire protocol: | ||||
encargs = [('one', encode(one),), ('two', encode(two),)] | ||||
# Create future for injection of encoded result: | ||||
encresref = future() | ||||
# Return encoded arguments and future: | ||||
yield encargs, encresref | ||||
Brodie Rao
|
r16683 | # Assuming the future to be filled with the result from the batched | ||
# request now. Decode it: | ||||
Peter Arrenbrecht
|
r14621 | yield decode(encresref.value) | ||
Brodie Rao
|
r16683 | The decorator returns a function which wraps this coroutine as a plain | ||
method, but adds the original method as an attribute called "batchable", | ||||
which is used by remotebatch to split the call into separate encoding and | ||||
decoding phases. | ||||
Peter Arrenbrecht
|
r14621 | ''' | ||
def plain(*args, **opts): | ||||
batchable = f(*args, **opts) | ||||
encargsorres, encresref = batchable.next() | ||||
if not encresref: | ||||
return encargsorres # a local result in this case | ||||
self = args[0] | ||||
encresref.set(self._submitone(f.func_name, encargsorres)) | ||||
return batchable.next() | ||||
setattr(plain, 'batchable', f) | ||||
return plain | ||||
Benoit Boissinot
|
r11597 | # list of nodes encoding / decoding | ||
def decodelist(l, sep=' '): | ||||
Peter Arrenbrecht
|
r13722 | if l: | ||
return map(bin, l.split(sep)) | ||||
return [] | ||||
Benoit Boissinot
|
r11597 | |||
def encodelist(l, sep=' '): | ||||
return sep.join(map(hex, l)) | ||||
Peter Arrenbrecht
|
r14622 | # batched call argument encoding | ||
def escapearg(plain): | ||||
return (plain | ||||
.replace(':', '::') | ||||
.replace(',', ':,') | ||||
.replace(';', ':;') | ||||
.replace('=', ':=')) | ||||
def unescapearg(escaped): | ||||
return (escaped | ||||
.replace(':=', '=') | ||||
.replace(':;', ';') | ||||
.replace(':,', ',') | ||||
.replace('::', ':')) | ||||
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 | ||||
# :plain: string with no transformation needed. | ||||
gboptsmap = {'heads': 'nodes', | ||||
'common': 'nodes', | ||||
Pierre-Yves David
|
r22353 | 'obsmarkers': 'boolean', | ||
Pierre-Yves David
|
r21657 | 'bundlecaps': 'csv', | ||
Pierre-Yves David
|
r21989 | 'listkeys': 'csv', | ||
'cg': 'boolean'} | ||||
Pierre-Yves David
|
r21646 | |||
Matt Mackall
|
r11586 | # client side | ||
Peter Arrenbrecht
|
r17192 | class wirepeer(peer.peerrepository): | ||
Peter Arrenbrecht
|
r14622 | |||
def batch(self): | ||||
return remotebatch(self) | ||||
def _submitbatch(self, req): | ||||
cmds = [] | ||||
for op, argsdict in req: | ||||
args = ','.join('%s=%s' % p for p in argsdict.iteritems()) | ||||
cmds.append('%s %s' % (op, args)) | ||||
rsp = self._call("batch", cmds=';'.join(cmds)) | ||||
return rsp.split(';') | ||||
def _submitone(self, op, args): | ||||
return self._call(op, **args) | ||||
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) | ||
Matt Mackall
|
r11586 | self._abort(error.RepoError(data)) | ||
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) | ||
Matt Mackall
|
r13047 | branchname = encoding.tolocal(urllib.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)) | ||||
def branches(self, nodes): | ||||
Benoit Boissinot
|
r11597 | n = encodelist(nodes) | ||
Matt Mackall
|
r11586 | d = self._call("branches", nodes=n) | ||
try: | ||||
Benoit Boissinot
|
r11597 | br = [tuple(decodelist(b)) for b in d.splitlines()] | ||
Matt Mackall
|
r11586 | return br | ||
Matt Mackall
|
r13726 | except ValueError: | ||
Matt Mackall
|
r11586 | self._abort(error.ResponseError(_("unexpected response:"), d)) | ||
def between(self, pairs): | ||||
Matt Mackall
|
r11587 | batch = 8 # avoid giant requests | ||
r = [] | ||||
for i in xrange(0, len(pairs), batch): | ||||
Benoit Boissinot
|
r11597 | n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]]) | ||
Matt Mackall
|
r11587 | d = self._call("between", pairs=n) | ||
try: | ||||
Benoit Boissinot
|
r11597 | r.extend(l and decodelist(l) or [] for l in d.splitlines()) | ||
Matt Mackall
|
r13726 | except ValueError: | ||
Matt Mackall
|
r11587 | self._abort(error.ResponseError(_("unexpected response:"), d)) | ||
return r | ||||
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 | |||
Peter Arrenbrecht
|
r14623 | @batchable | ||
Matt Mackall
|
r11586 | def listkeys(self, namespace): | ||
if not self.capable('pushkey'): | ||||
Peter Arrenbrecht
|
r14623 | yield {}, None | ||
f = future() | ||||
Pierre-Yves David
|
r17293 | self.ui.debug('preparing listkeys for "%s"\n' % namespace) | ||
Augie Fackler
|
r20671 | yield {'namespace': encoding.fromlocal(namespace)}, f | ||
Peter Arrenbrecht
|
r14623 | d = f.value | ||
Pierre-Yves David
|
r21653 | yield pushkeymod.decodekeys(d) | ||
Matt Mackall
|
r11586 | |||
Matt Mackall
|
r11588 | def stream_out(self): | ||
return self._callstream('stream_out') | ||||
Matt Mackall
|
r11591 | def changegroup(self, nodes, kind): | ||
Benoit Boissinot
|
r11597 | n = encodelist(nodes) | ||
Pierre-Yves David
|
r20905 | f = self._callcompressable("changegroup", roots=n) | ||
Sune Foldager
|
r22390 | return changegroupmod.cg1unpacker(f, 'UN') | ||
Matt Mackall
|
r11591 | |||
def changegroupsubset(self, bases, heads, kind): | ||||
self.requirecap('changegroupsubset', _('look up remote changes')) | ||||
Benoit Boissinot
|
r11597 | bases = encodelist(bases) | ||
heads = encodelist(heads) | ||||
Pierre-Yves David
|
r20905 | f = self._callcompressable("changegroupsubset", | ||
bases=bases, heads=heads) | ||||
Sune Foldager
|
r22390 | return changegroupmod.cg1unpacker(f, 'UN') | ||
Matt Mackall
|
r11591 | |||
Pierre-Yves David
|
r21646 | def getbundle(self, source, **kwargs): | ||
Peter Arrenbrecht
|
r13741 | self.requirecap('getbundle', _('look up remote changes')) | ||
opts = {} | ||||
Pierre-Yves David
|
r21646 | for key, value in kwargs.iteritems(): | ||
if value is None: | ||||
continue | ||||
keytype = gboptsmap.get(key) | ||||
if keytype is None: | ||||
assert False, 'unexpected' | ||||
elif keytype == 'nodes': | ||||
value = encodelist(value) | ||||
elif keytype == 'csv': | ||||
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 | ||||
Pierre-Yves David
|
r20905 | f = self._callcompressable("getbundle", **opts) | ||
Pierre-Yves David
|
r21646 | bundlecaps = kwargs.get('bundlecaps') | ||
Pierre-Yves David
|
r23009 | if bundlecaps is not None and 'HG2Y' in bundlecaps: | ||
Pierre-Yves David
|
r21069 | return bundle2.unbundle20(self.ui, f) | ||
else: | ||||
Sune Foldager
|
r22390 | return changegroupmod.cg1unpacker(f, 'UN') | ||
Peter Arrenbrecht
|
r13741 | |||
Matt Mackall
|
r11592 | def unbundle(self, cg, heads, source): | ||
'''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 | ||||
result of the push (see localrepository.addchangegroup()). | ||||
When pushing a bundle20 stream, return a bundle20 stream.''' | ||||
Matt Mackall
|
r11592 | |||
Martin Geisler
|
r14419 | if heads != ['force'] and self.capable('unbundlehash'): | ||
Shuhei Takahashi
|
r13942 | heads = encodelist(['hashed', | ||
util.sha1(''.join(sorted(heads))).digest()]) | ||||
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) | ||||
ret = bundle2.unbundle20(self.ui, stream) | ||||
Matt Mackall
|
r11592 | return ret | ||
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: | ||||
opts['three'] = three | ||||
if four is not None: | ||||
opts['four'] = four | ||||
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 | ||||
The command is expected to return a stream. | ||||
returns the server reply as a file like object.""" | ||||
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. | ||
Dirkjan Ochtman
|
r11625 | class streamres(object): | ||
Pierre-Yves David
|
r20902 | """wireproto reply: binary stream | ||
The call was successful and the result is a stream. | ||||
Iterate on the `self.gen` attribute to retrieve chunks. | ||||
""" | ||||
Dirkjan Ochtman
|
r11625 | def __init__(self, gen): | ||
self.gen = gen | ||||
class pushres(object): | ||||
Pierre-Yves David
|
r20902 | """wireproto reply: success with simple integer return | ||
The call was successful and returned an integer contained in `self.res`. | ||||
""" | ||||
Dirkjan Ochtman
|
r11625 | def __init__(self, res): | ||
self.res = res | ||||
Benoit Boissinot
|
r12703 | class pusherr(object): | ||
Pierre-Yves David
|
r20902 | """wireproto reply: failure | ||
The call failed. The `self.res` attribute contains the error message. | ||||
""" | ||||
Benoit Boissinot
|
r12703 | def __init__(self, res): | ||
self.res = res | ||||
Andrew Pritchard
|
r15017 | class ooberror(object): | ||
Pierre-Yves David
|
r20902 | """wireproto reply: failure of a batch of operation | ||
Something failed during a batch call. The error message is stored in | ||||
`self.message`. | ||||
""" | ||||
Andrew Pritchard
|
r15017 | def __init__(self, message): | ||
self.message = message | ||||
Matt Mackall
|
r11581 | def dispatch(repo, proto, command): | ||
Kevin Bullock
|
r18382 | repo = repo.filtered("served") | ||
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: | ||||
Pierre-Yves David
|
r21728 | sys.stderr.write("warning: %s ignored unexpected arguments %s\n" | ||
Peter Arrenbrecht
|
r13721 | % (cmd, ",".join(others))) | ||
return opts | ||||
Pierre-Yves David
|
r20906 | # list of commands | ||
commands = {} | ||||
def wireprotocommand(name, args=''): | ||||
Mads Kiilerich
|
r21024 | """decorator for wire protocol command""" | ||
Pierre-Yves David
|
r20906 | def register(func): | ||
commands[name] = (func, args) | ||||
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('=') | ||||
vals[n] = unescapearg(v) | ||||
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 | ||||
Peter Arrenbrecht
|
r14622 | res.append(escapearg(result)) | ||
return ';'.join(res) | ||||
Pierre-Yves David
|
r20908 | @wireprotocommand('between', 'pairs') | ||
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") | ||
Matt Mackall
|
r11581 | return "".join(r) | ||
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(): | ||||
Matt Mackall
|
r13047 | branchname = urllib.quote(encoding.fromlocal(branch)) | ||
Benoit Boissinot
|
r11597 | branchnodes = encodelist(nodes) | ||
heads.append('%s %s' % (branchname, branchnodes)) | ||||
Matt Mackall
|
r11581 | return '\n'.join(heads) | ||
Pierre-Yves David
|
r20910 | @wireprotocommand('branches', 'nodes') | ||
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") | ||
Matt Mackall
|
r11581 | return "".join(r) | ||
Pierre-Yves David
|
r20774 | |||
wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey', | ||||
'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) | ||||
Dirkjan Ochtman
|
r11627 | if _allowstream(repo.ui): | ||
Benoit Allard
|
r16361 | if repo.ui.configbool('server', 'preferuncompressed', False): | ||
caps.append('stream-preferred') | ||||
Sune Foldager
|
r12296 | requiredformats = repo.requirements & repo.supportedformats | ||
# if our local revlogs are just revlogv1, add 'stream' cap | ||||
if not requiredformats - set(('revlogv1',)): | ||||
caps.append('stream') | ||||
# otherwise, add 'streamreqs' detailing our local revlog format | ||||
else: | ||||
caps.append('streamreqs=%s' % ','.join(requiredformats)) | ||||
Pierre-Yves David
|
r21148 | if repo.ui.configbool('experimental', 'bundle2-exp', False): | ||
Pierre-Yves David
|
r22342 | capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo)) | ||
Pierre-Yves David
|
r21145 | caps.append('bundle2-exp=' + urllib.quote(capsblob)) | ||
Matt Mackall
|
r11594 | caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority)) | ||
Steven Brown
|
r14093 | caps.append('httpheader=1024') | ||
Pierre-Yves David
|
r20775 | return caps | ||
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): | ||
return ' '.join(_capabilities(repo, proto)) | ||||
Matt Mackall
|
r11594 | |||
Pierre-Yves David
|
r20912 | @wireprotocommand('changegroup', 'roots') | ||
Matt Mackall
|
r11584 | def changegroup(repo, proto, roots): | ||
Benoit Boissinot
|
r11597 | nodes = decodelist(roots) | ||
Pierre-Yves David
|
r20931 | cg = changegroupmod.changegroup(repo, nodes, 'serve') | ||
Dirkjan Ochtman
|
r11625 | return streamres(proto.groupchunks(cg)) | ||
Matt Mackall
|
r11584 | |||
Pierre-Yves David
|
r20913 | @wireprotocommand('changegroupsubset', 'bases heads') | ||
Matt Mackall
|
r11584 | def changegroupsubset(repo, proto, bases, heads): | ||
Benoit Boissinot
|
r11597 | bases = decodelist(bases) | ||
heads = decodelist(heads) | ||||
Pierre-Yves David
|
r20927 | cg = changegroupmod.changegroupsubset(repo, bases, heads, 'serve') | ||
Dirkjan Ochtman
|
r11625 | return streamres(proto.groupchunks(cg)) | ||
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) | ||||
return repo.debugwireargs(one, two, **opts) | ||||
Peter Arrenbrecht
|
r13720 | |||
Pierre-Yves David
|
r21615 | # List of options accepted by getbundle. | ||
# | ||||
# Meant to be extended by extensions. It is the extension's responsibility to | ||||
# ensure such options are properly processed in exchange.getbundle. | ||||
gboptslist = ['heads', 'common', 'bundlecaps'] | ||||
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': | ||
Benoit Boissinot
|
r19201 | opts[k] = set(v.split(',')) | ||
Pierre-Yves David
|
r21988 | elif keytype == 'boolean': | ||
Pierre-Yves David
|
r22351 | opts[k] = bool(v) | ||
Pierre-Yves David
|
r21646 | elif keytype != 'plain': | ||
raise KeyError('unknown getbundle option type %s' | ||||
% keytype) | ||||
Pierre-Yves David
|
r21069 | cg = exchange.getbundle(repo, 'serve', **opts) | ||
Peter Arrenbrecht
|
r13741 | return streamres(proto.groupchunks(cg)) | ||
Pierre-Yves David
|
r20916 | @wireprotocommand('heads') | ||
Matt Mackall
|
r11583 | def heads(repo, proto): | ||
Pierre-Yves David
|
r18281 | h = repo.heads() | ||
Benoit Boissinot
|
r11597 | return encodelist(h) + "\n" | ||
Matt Mackall
|
r11581 | |||
Pierre-Yves David
|
r20917 | @wireprotocommand('hello') | ||
Matt Mackall
|
r11594 | def hello(repo, proto): | ||
'''the hello command returns a set of lines describing various | ||||
interesting things about the server, in an RFC822-like format. | ||||
Currently the only one defined is "capabilities", which | ||||
consists of a line in the form: | ||||
capabilities: space separated list of tokens | ||||
''' | ||||
return "capabilities: %s\n" % (capabilities(repo, proto)) | ||||
Pierre-Yves David
|
r20919 | @wireprotocommand('listkeys', 'namespace') | ||
Matt Mackall
|
r11583 | def listkeys(repo, proto, namespace): | ||
Andreas Freimuth
|
r15217 | d = repo.listkeys(encoding.tolocal(namespace)).items() | ||
Pierre-Yves David
|
r21651 | return 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 | ||
except Exception, inst: | ||||
r = str(inst) | ||||
success = 0 | ||||
return "%s %s\n" % (success, r) | ||||
Pierre-Yves David
|
r20918 | @wireprotocommand('known', 'nodes *') | ||
Peter Arrenbrecht
|
r14436 | def known(repo, proto, nodes, others): | ||
Peter Arrenbrecht
|
r13723 | return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes))) | ||
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 | ||||
if len(new) == 20 and new.encode('string-escape') != new: | ||||
# 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 | ||||
Wagner Bruna
|
r17793 | if util.safehasattr(proto, 'restore'): | ||
proto.redirect() | ||||
try: | ||||
r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key), | ||||
encoding.tolocal(old), new) or False | ||||
except util.Abort: | ||||
r = False | ||||
output = proto.restore() | ||||
return '%s\n%s' % (int(r), output) | ||||
Andreas Freimuth
|
r15217 | r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key), | ||
encoding.tolocal(old), new) | ||||
Matt Mackall
|
r11581 | return '%s\n' % int(r) | ||
Dirkjan Ochtman
|
r11627 | def _allowstream(ui): | ||
return ui.configbool('server', 'uncompressed', True, untrusted=True) | ||||
Durham Goode
|
r19176 | def _walkstreamfiles(repo): | ||
# this is it's own function so extensions can override it | ||||
return repo.store.walk() | ||||
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. | ||||
The format is simple: the server writes out a line with the amount | ||||
Mads Kiilerich
|
r17424 | of files, then the total amount of bytes to be transferred (separated | ||
Dirkjan Ochtman
|
r11627 | by a space). Then, for each file, the server first writes the filename | ||
Mads Kiilerich
|
r21024 | and file size (separated by the null character), then the file contents. | ||
Dirkjan Ochtman
|
r11627 | ''' | ||
if not _allowstream(repo.ui): | ||||
return '1\n' | ||||
entries = [] | ||||
total_bytes = 0 | ||||
try: | ||||
# get consistent snapshot of repo, lock during scan | ||||
lock = repo.lock() | ||||
try: | ||||
repo.ui.debug('scanning\n') | ||||
Durham Goode
|
r19176 | for name, ename, size in _walkstreamfiles(repo): | ||
Mads Kiilerich
|
r18381 | if size: | ||
entries.append((name, size)) | ||||
total_bytes += size | ||||
Dirkjan Ochtman
|
r11627 | finally: | ||
lock.release() | ||||
except error.LockError: | ||||
return '2\n' # error: 2 | ||||
def streamer(repo, entries, total): | ||||
'''stream out all metadata files in repository.''' | ||||
yield '0\n' # success | ||||
repo.ui.debug('%d files, %d bytes to transfer\n' % | ||||
(len(entries), total_bytes)) | ||||
yield '%d %d\n' % (len(entries), total_bytes) | ||||
Bryan O'Sullivan
|
r17556 | |||
sopener = repo.sopener | ||||
oldaudit = sopener.mustaudit | ||||
Bryan O'Sullivan
|
r17558 | debugflag = repo.ui.debugflag | ||
Bryan O'Sullivan
|
r17556 | sopener.mustaudit = False | ||
try: | ||||
for name, size in entries: | ||||
Bryan O'Sullivan
|
r17558 | if debugflag: | ||
repo.ui.debug('sending %s (%d bytes)\n' % (name, size)) | ||||
Bryan O'Sullivan
|
r17556 | # partially encode name over the wire for backwards compat | ||
yield '%s\0%d\n' % (store.encodedir(name), size) | ||||
Bryan O'Sullivan
|
r17557 | if size <= 65536: | ||
Patrick Mezard
|
r17567 | fp = sopener(name) | ||
try: | ||||
data = fp.read(size) | ||||
finally: | ||||
fp.close() | ||||
yield data | ||||
Bryan O'Sullivan
|
r17557 | else: | ||
for chunk in util.filechunkiter(sopener(name), limit=size): | ||||
yield chunk | ||||
Thomas Arendsen Hein
|
r17603 | # replace with "finally:" when support for python 2.4 has been dropped | ||
except Exception: | ||||
Bryan O'Sullivan
|
r17556 | sopener.mustaudit = oldaudit | ||
Thomas Arendsen Hein
|
r17603 | raise | ||
sopener.mustaudit = oldaudit | ||||
Dirkjan Ochtman
|
r11627 | |||
return streamres(streamer(repo, entries, total_bytes)) | ||||
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 | |||
Pierre-Yves David
|
r20967 | try: | ||
proto.redirect() | ||||
Matt Mackall
|
r11593 | |||
Pierre-Yves David
|
r20967 | exchange.check_heads(repo, their_heads, 'preparing changes') | ||
Benoit Boissinot
|
r12702 | |||
Pierre-Yves David
|
r20967 | # write bundle data to temporary file because it can be big | ||
fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') | ||||
fp = os.fdopen(fd, 'wb+') | ||||
r = 0 | ||||
Matt Mackall
|
r11593 | try: | ||
Pierre-Yves David
|
r20967 | proto.getfile(fp) | ||
Pierre-Yves David
|
r20968 | fp.seek(0) | ||
Pierre-Yves David
|
r21064 | gen = exchange.readbundle(repo.ui, fp, None) | ||
Pierre-Yves David
|
r20968 | r = exchange.unbundle(repo, gen, their_heads, 'serve', | ||
proto._client()) | ||||
Pierre-Yves David
|
r21075 | if util.safehasattr(r, 'addpart'): | ||
Mads Kiilerich
|
r23139 | # The return looks streamable, we are in the bundle2 case and | ||
Pierre-Yves David
|
r21075 | # should return a stream. | ||
return streamres(r.getchunks()) | ||||
Pierre-Yves David
|
r20967 | return pushres(r) | ||
Matt Mackall
|
r11593 | finally: | ||
Pierre-Yves David
|
r20967 | fp.close() | ||
os.unlink(tempname) | ||||
Pierre-Yves David
|
r21618 | except error.BundleValueError, exc: | ||
Pierre-Yves David
|
r21183 | bundler = bundle2.bundle20(repo.ui) | ||
Pierre-Yves David
|
r21619 | errpart = bundler.newpart('B2X:ERROR:UNSUPPORTEDCONTENT') | ||
Pierre-Yves David
|
r21627 | if exc.parttype is not None: | ||
errpart.addparam('parttype', exc.parttype) | ||||
Pierre-Yves David
|
r21622 | if exc.params: | ||
errpart.addparam('params', '\0'.join(exc.params)) | ||||
Pierre-Yves David
|
r21183 | return streamres(bundler.getchunks()) | ||
Pierre-Yves David
|
r20969 | except util.Abort, inst: | ||
# The old code we moved used sys.stderr directly. | ||||
Mads Kiilerich
|
r21024 | # We did not change it to minimise code change. | ||
Pierre-Yves David
|
r20969 | # This need to be moved to something proper. | ||
# Feel free to do it. | ||||
Pierre-Yves David
|
r21177 | if getattr(inst, 'duringunbundle2', False): | ||
bundler = bundle2.bundle20(repo.ui) | ||||
manargs = [('message', str(inst))] | ||||
advargs = [] | ||||
if inst.hint is not None: | ||||
advargs.append(('hint', inst.hint)) | ||||
bundler.addpart(bundle2.bundlepart('B2X:ERROR:ABORT', | ||||
manargs, advargs)) | ||||
return streamres(bundler.getchunks()) | ||||
else: | ||||
sys.stderr.write("abort: %s\n" % inst) | ||||
return pushres(0) | ||||
Pierre-Yves David
|
r21184 | except error.PushRaced, exc: | ||
Pierre-Yves David
|
r21186 | if getattr(exc, 'duringunbundle2', False): | ||
bundler = bundle2.bundle20(repo.ui) | ||||
Pierre-Yves David
|
r21600 | bundler.newpart('B2X:ERROR:PUSHRACED', [('message', str(exc))]) | ||
Pierre-Yves David
|
r21186 | return streamres(bundler.getchunks()) | ||
else: | ||||
return pusherr(str(exc)) | ||||