wireprotov1server.py
747 lines
| 26.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / wireprotov1server.py
|
r37803 | # wireprotov1server.py - Wire protocol version 1 server functionality | ||
# | ||||
# 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. | ||||
from __future__ import absolute_import | ||||
|
r41609 | import binascii | ||
|
r37803 | import os | ||
from .i18n import _ | ||||
from .node import ( | ||||
hex, | ||||
nullid, | ||||
) | ||||
|
r43359 | from .pycompat import getattr | ||
|
r37803 | |||
from . import ( | ||||
bundle2, | ||||
r46369 | bundlecaches, | |||
|
r37803 | changegroup as changegroupmod, | ||
discovery, | ||||
encoding, | ||||
error, | ||||
exchange, | ||||
pushkey as pushkeymod, | ||||
pycompat, | ||||
streamclone, | ||||
util, | ||||
wireprototypes, | ||||
) | ||||
from .utils import ( | ||||
procutil, | ||||
stringutil, | ||||
) | ||||
urlerr = util.urlerr | ||||
urlreq = util.urlreq | ||||
|
r43347 | bundle2requiredmain = _(b'incompatible Mercurial client; bundle2 required') | ||
|
r43346 | bundle2requiredhint = _( | ||
|
r43387 | b'see https://www.mercurial-scm.org/wiki/IncompatibleClient' | ||
|
r43346 | ) | ||
|
r43347 | bundle2required = b'%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint) | ||
|
r37803 | |||
|
r43346 | |||
|
r37803 | def clientcompressionsupport(proto): | ||
"""Returns a list of compression methods supported by the client. | ||||
Returns a list of the compression methods supported by the client | ||||
according to the protocol capabilities. If no such capability has | ||||
been announced, fallback to the default of zlib and uncompressed. | ||||
""" | ||||
for cap in proto.getprotocaps(): | ||||
|
r43347 | if cap.startswith(b'comp='): | ||
return cap[5:].split(b',') | ||||
return [b'zlib', b'none'] | ||||
|
r37803 | |||
|
r43346 | |||
|
r37803 | # wire protocol command can either return a string or one of these classes. | ||
|
r43346 | |||
|
r37803 | 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. | ||||
""" | ||||
|
r43347 | viewconfig = repo.ui.config(b'server', b'view') | ||
|
r42006 | return repo.filtered(viewconfig) | ||
|
r37803 | |||
|
r43346 | |||
|
r37803 | def dispatch(repo, proto, command): | ||
repo = getdispatchrepo(repo, proto, command) | ||||
func, spec = commands[command] | ||||
args = proto.getargs(spec) | ||||
return func(repo, proto, *args) | ||||
|
r43346 | |||
|
r37803 | def options(cmd, keys, others): | ||
opts = {} | ||||
for k in keys: | ||||
if k in others: | ||||
opts[k] = others[k] | ||||
del others[k] | ||||
if others: | ||||
|
r43346 | procutil.stderr.write( | ||
|
r43347 | b"warning: %s ignored unexpected arguments %s\n" | ||
% (cmd, b",".join(others)) | ||||
|
r43346 | ) | ||
|
r37803 | return opts | ||
|
r43346 | |||
|
r37803 | 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 | ||||
|
r43347 | gd = b'generaldelta' in repo.requirements | ||
|
r37803 | |||
if gd: | ||||
|
r43347 | v = ui.configbool(b'server', b'bundle1gd.%s' % action) | ||
|
r37803 | if v is not None: | ||
return v | ||||
|
r43347 | v = ui.configbool(b'server', b'bundle1.%s' % action) | ||
|
r37803 | if v is not None: | ||
return v | ||||
if gd: | ||||
|
r43347 | v = ui.configbool(b'server', b'bundle1gd') | ||
|
r37803 | if v is not None: | ||
return v | ||||
|
r43347 | return ui.configbool(b'server', b'bundle1') | ||
|
r37803 | |||
|
r43346 | |||
|
r37803 | commands = wireprototypes.commanddict() | ||
|
r43346 | |||
|
r43347 | def wireprotocommand(name, args=None, permission=b'push'): | ||
|
r37803 | """Decorator to declare a wire protocol command. | ||
``name`` is the name of the wire protocol command being provided. | ||||
``args`` defines the named arguments accepted by the command. It is | ||||
a space-delimited list of argument names. ``*`` denotes a special value | ||||
that says to accept all named arguments. | ||||
``permission`` defines the permission type needed to run this command. | ||||
Can be ``push`` or ``pull``. These roughly map to read-write and read-only, | ||||
respectively. Default is to assume command requires ``push`` permissions | ||||
because otherwise commands not declaring their permissions could modify | ||||
a repository that is supposed to be read-only. | ||||
""" | ||||
|
r43346 | transports = { | ||
|
r43347 | k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 1 | ||
|
r43346 | } | ||
|
r37803 | |||
# Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to | ||||
# SSHv2. | ||||
# TODO undo this hack when SSH is using the unified frame protocol. | ||||
if name == b'batch': | ||||
transports.add(wireprototypes.SSHV2) | ||||
|
r43347 | if permission not in (b'push', b'pull'): | ||
|
r43346 | raise error.ProgrammingError( | ||
|
r43347 | b'invalid wire protocol permission; ' | ||
b'got %s; expected "push" or "pull"' % permission | ||||
|
r43346 | ) | ||
|
r37803 | |||
if args is None: | ||||
|
r43347 | args = b'' | ||
|
r37803 | |||
if not isinstance(args, bytes): | ||||
|
r43346 | raise error.ProgrammingError( | ||
|
r43387 | b'arguments for version 1 commands must be declared as bytes' | ||
|
r43346 | ) | ||
|
r37803 | |||
def register(func): | ||||
if name in commands: | ||||
|
r43346 | raise error.ProgrammingError( | ||
|
r43387 | b'%s command already registered for version 1' % name | ||
|
r43346 | ) | ||
|
r37803 | commands[name] = wireprototypes.commandentry( | ||
|
r43346 | func, args=args, transports=transports, permission=permission | ||
) | ||||
|
r37803 | |||
return func | ||||
|
r43346 | |||
|
r37803 | return register | ||
|
r43346 | |||
|
r37803 | # TODO define a more appropriate permissions type to use for this. | ||
|
r43347 | @wireprotocommand(b'batch', b'cmds *', permission=b'pull') | ||
|
r37803 | def batch(repo, proto, cmds, others): | ||
unescapearg = wireprototypes.unescapebatcharg | ||||
res = [] | ||||
|
r43347 | for pair in cmds.split(b';'): | ||
op, args = pair.split(b' ', 1) | ||||
|
r37803 | vals = {} | ||
|
r43347 | for a in args.split(b','): | ||
|
r37803 | if a: | ||
|
r43347 | n, v = a.split(b'=') | ||
|
r37803 | vals[unescapearg(n)] = unescapearg(v) | ||
func, spec = commands[op] | ||||
# Validate that client has permissions to perform this command. | ||||
perm = commands[op].permission | ||||
|
r43347 | assert perm in (b'push', b'pull') | ||
|
r37803 | proto.checkperm(perm) | ||
if spec: | ||||
keys = spec.split() | ||||
data = {} | ||||
for k in keys: | ||||
|
r43347 | if k == b'*': | ||
|
r37803 | star = {} | ||
for key in vals.keys(): | ||||
if key not in keys: | ||||
star[key] = vals[key] | ||||
|
r43347 | data[b'*'] = star | ||
|
r37803 | else: | ||
data[k] = vals[k] | ||||
result = func(repo, proto, *[data[k] for k in keys]) | ||||
else: | ||||
result = func(repo, proto) | ||||
if isinstance(result, wireprototypes.ooberror): | ||||
return result | ||||
# For now, all batchable commands must return bytesresponse or | ||||
# raw bytes (for backwards compatibility). | ||||
assert isinstance(result, (wireprototypes.bytesresponse, bytes)) | ||||
if isinstance(result, wireprototypes.bytesresponse): | ||||
result = result.data | ||||
res.append(wireprototypes.escapebatcharg(result)) | ||||
|
r43347 | return wireprototypes.bytesresponse(b';'.join(res)) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'between', b'pairs', permission=b'pull') | ||
|
r37803 | def between(repo, proto, pairs): | ||
|
r43347 | pairs = [wireprototypes.decodelist(p, b'-') for p in pairs.split(b" ")] | ||
|
r37803 | r = [] | ||
for b in repo.between(pairs): | ||||
|
r43347 | r.append(wireprototypes.encodelist(b) + b"\n") | ||
|
r37803 | |||
|
r43347 | return wireprototypes.bytesresponse(b''.join(r)) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'branchmap', permission=b'pull') | ||
|
r37803 | def branchmap(repo, proto): | ||
branchmap = repo.branchmap() | ||||
heads = [] | ||||
|
r43376 | for branch, nodes in pycompat.iteritems(branchmap): | ||
|
r37803 | branchname = urlreq.quote(encoding.fromlocal(branch)) | ||
branchnodes = wireprototypes.encodelist(nodes) | ||||
|
r43347 | heads.append(b'%s %s' % (branchname, branchnodes)) | ||
|
r37803 | |||
|
r43347 | return wireprototypes.bytesresponse(b'\n'.join(heads)) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'branches', b'nodes', permission=b'pull') | ||
|
r37803 | def branches(repo, proto, nodes): | ||
nodes = wireprototypes.decodelist(nodes) | ||||
r = [] | ||||
for b in repo.branches(nodes): | ||||
|
r43347 | r.append(wireprototypes.encodelist(b) + b"\n") | ||
|
r37803 | |||
|
r43347 | return wireprototypes.bytesresponse(b''.join(r)) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'clonebundles', b'', permission=b'pull') | ||
|
r37803 | 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. | ||||
""" | ||||
return wireprototypes.bytesresponse( | ||||
r46370 | repo.vfs.tryread(bundlecaches.CB_MANIFEST_FILE) | |||
|
r43346 | ) | ||
|
r37803 | |||
|
r43346 | wireprotocaps = [ | ||
|
r43347 | b'lookup', | ||
b'branchmap', | ||||
b'pushkey', | ||||
b'known', | ||||
b'getbundle', | ||||
b'unbundlehash', | ||||
|
r43346 | ] | ||
|
r37803 | |||
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` | ||||
command without any other action needed. | ||||
""" | ||||
# copy to prevent modification of the global list | ||||
caps = list(wireprotocaps) | ||||
# Command of same name as capability isn't exposed to version 1 of | ||||
# transports. So conditionally add it. | ||||
|
r43347 | if commands.commandavailable(b'changegroupsubset', proto): | ||
caps.append(b'changegroupsubset') | ||||
|
r37803 | |||
if streamclone.allowservergeneration(repo): | ||||
|
r43347 | if repo.ui.configbool(b'server', b'preferuncompressed'): | ||
caps.append(b'stream-preferred') | ||||
|
r37803 | requiredformats = repo.requirements & repo.supportedformats | ||
# if our local revlogs are just revlogv1, add 'stream' cap | ||||
|
r43347 | if not requiredformats - {b'revlogv1'}: | ||
caps.append(b'stream') | ||||
|
r37803 | # otherwise, add 'streamreqs' detailing our local revlog format | ||
else: | ||||
|
r43347 | caps.append(b'streamreqs=%s' % b','.join(sorted(requiredformats))) | ||
if repo.ui.configbool(b'experimental', b'bundle2-advertise'): | ||||
capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=b'server')) | ||||
caps.append(b'bundle2=' + urlreq.quote(capsblob)) | ||||
caps.append(b'unbundle=%s' % b','.join(bundle2.bundlepriority)) | ||||
|
r37803 | |||
|
r43347 | if repo.ui.configbool(b'experimental', b'narrow'): | ||
|
r40111 | caps.append(wireprototypes.NARROWCAP) | ||
|
r43347 | if repo.ui.configbool(b'experimental', b'narrowservebrokenellipses'): | ||
|
r40111 | caps.append(wireprototypes.ELLIPSESCAP) | ||
|
r37803 | return proto.addcapabilities(repo, caps) | ||
|
r43346 | |||
|
r37803 | # If you are writing an extension and consider wrapping this function. Wrap | ||
# `_capabilities` instead. | ||||
|
r43347 | @wireprotocommand(b'capabilities', permission=b'pull') | ||
|
r37803 | def capabilities(repo, proto): | ||
caps = _capabilities(repo, proto) | ||||
|
r43347 | return wireprototypes.bytesresponse(b' '.join(sorted(caps))) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'changegroup', b'roots', permission=b'pull') | ||
|
r37803 | def changegroup(repo, proto, roots): | ||
nodes = wireprototypes.decodelist(roots) | ||||
|
r43346 | outgoing = discovery.outgoing( | ||
|
r45704 | repo, missingroots=nodes, ancestorsof=repo.heads() | ||
|
r43346 | ) | ||
|
r43347 | cg = changegroupmod.makechangegroup(repo, outgoing, b'01', b'serve') | ||
gen = iter(lambda: cg.read(32768), b'') | ||||
|
r37803 | return wireprototypes.streamres(gen=gen) | ||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'changegroupsubset', b'bases heads', permission=b'pull') | ||
|
r37803 | def changegroupsubset(repo, proto, bases, heads): | ||
bases = wireprototypes.decodelist(bases) | ||||
heads = wireprototypes.decodelist(heads) | ||||
|
r45704 | outgoing = discovery.outgoing(repo, missingroots=bases, ancestorsof=heads) | ||
|
r43347 | cg = changegroupmod.makechangegroup(repo, outgoing, b'01', b'serve') | ||
gen = iter(lambda: cg.read(32768), b'') | ||||
|
r37803 | return wireprototypes.streamres(gen=gen) | ||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'debugwireargs', b'one two *', permission=b'pull') | ||
|
r37803 | def debugwireargs(repo, proto, one, two, others): | ||
# only accept optional args from the known set | ||||
|
r43347 | opts = options(b'debugwireargs', [b'three', b'four'], others) | ||
|
r43346 | return wireprototypes.bytesresponse( | ||
repo.debugwireargs(one, two, **pycompat.strkwargs(opts)) | ||||
) | ||||
|
r37803 | |||
def find_pullbundle(repo, proto, opts, clheads, heads, common): | ||||
"""Return a file object for the first matching pullbundle. | ||||
Pullbundles are specified in .hg/pullbundles.manifest similar to | ||||
clonebundles. | ||||
For each entry, the bundle specification is checked for compatibility: | ||||
- Client features vs the BUNDLESPEC. | ||||
- Revisions shared with the clients vs base revisions of the bundle. | ||||
A bundle can be applied only if all its base revisions are known by | ||||
the client. | ||||
- At least one leaf of the bundle's DAG is missing on the client. | ||||
- Every leaf of the bundle's DAG is part of node set the client wants. | ||||
E.g. do not send a bundle of all changes if the client wants only | ||||
one specific branch of many. | ||||
""" | ||||
|
r43346 | |||
|
r37803 | def decodehexstring(s): | ||
|
r43347 | return {binascii.unhexlify(h) for h in s.split(b';')} | ||
|
r37803 | |||
|
r43347 | manifest = repo.vfs.tryread(b'pullbundles.manifest') | ||
|
r37803 | if not manifest: | ||
return None | ||||
r46369 | res = bundlecaches.parseclonebundlesmanifest(repo, manifest) | |||
res = bundlecaches.filterclonebundleentries(repo, res) | ||||
|
r37803 | if not res: | ||
return None | ||||
|
r45305 | cl = repo.unfiltered().changelog | ||
|
r37803 | heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True) | ||
common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True) | ||||
compformats = clientcompressionsupport(proto) | ||||
for entry in res: | ||||
|
r43347 | comp = entry.get(b'COMPRESSION') | ||
|
r38700 | altcomp = util.compengines._bundlenames.get(comp) | ||
if comp and comp not in compformats and altcomp not in compformats: | ||||
|
r37803 | continue | ||
# No test yet for VERSION, since V2 is supported by any client | ||||
# that advertises partial pulls | ||||
|
r43347 | if b'heads' in entry: | ||
|
r37803 | try: | ||
|
r43347 | bundle_heads = decodehexstring(entry[b'heads']) | ||
|
r37803 | except TypeError: | ||
# Bad heads entry | ||||
continue | ||||
if bundle_heads.issubset(common): | ||||
|
r43346 | continue # Nothing new | ||
|
r37803 | if all(cl.rev(rev) in common_anc for rev in bundle_heads): | ||
|
r43346 | continue # Still nothing new | ||
if any( | ||||
cl.rev(rev) not in heads_anc and cl.rev(rev) not in common_anc | ||||
for rev in bundle_heads | ||||
): | ||||
|
r37803 | continue | ||
|
r43347 | if b'bases' in entry: | ||
|
r37803 | try: | ||
|
r43347 | bundle_bases = decodehexstring(entry[b'bases']) | ||
|
r37803 | except TypeError: | ||
# Bad bases entry | ||||
continue | ||||
if not all(cl.rev(rev) in common_anc for rev in bundle_bases): | ||||
continue | ||||
|
r43347 | path = entry[b'URL'] | ||
repo.ui.debug(b'sending pullbundle "%s"\n' % path) | ||||
|
r37803 | try: | ||
return repo.vfs.open(path) | ||||
except IOError: | ||||
|
r43347 | repo.ui.debug(b'pullbundle "%s" not accessible\n' % path) | ||
|
r37803 | continue | ||
return None | ||||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'getbundle', b'*', permission=b'pull') | ||
|
r37803 | def getbundle(repo, proto, others): | ||
|
r43346 | opts = options( | ||
|
r43347 | b'getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(), others | ||
|
r43346 | ) | ||
|
r43376 | for k, v in pycompat.iteritems(opts): | ||
|
r37803 | keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k] | ||
|
r43347 | if keytype == b'nodes': | ||
|
r37803 | opts[k] = wireprototypes.decodelist(v) | ||
|
r43347 | elif keytype == b'csv': | ||
opts[k] = list(v.split(b',')) | ||||
elif keytype == b'scsv': | ||||
opts[k] = set(v.split(b',')) | ||||
elif keytype == b'boolean': | ||||
|
r37803 | # Client should serialize False as '0', which is a non-empty string | ||
# so it evaluates as a True bool. | ||||
|
r43347 | if v == b'0': | ||
|
r37803 | opts[k] = False | ||
else: | ||||
opts[k] = bool(v) | ||||
|
r43347 | elif keytype != b'plain': | ||
raise KeyError(b'unknown getbundle option type %s' % keytype) | ||||
|
r37803 | |||
|
r43347 | if not bundle1allowed(repo, b'pull'): | ||
if not exchange.bundle2requested(opts.get(b'bundlecaps')): | ||||
if proto.name == b'http-v1': | ||||
|
r37803 | return wireprototypes.ooberror(bundle2required) | ||
|
r43346 | raise error.Abort(bundle2requiredmain, hint=bundle2requiredhint) | ||
|
r37803 | |||
try: | ||||
clheads = set(repo.changelog.heads()) | ||||
|
r43347 | heads = set(opts.get(b'heads', set())) | ||
common = set(opts.get(b'common', set())) | ||||
|
r37803 | common.discard(nullid) | ||
|
r43346 | if ( | ||
|
r43347 | repo.ui.configbool(b'server', b'pullbundle') | ||
and b'partial-pull' in proto.getprotocaps() | ||||
|
r43346 | ): | ||
|
r37803 | # Check if a pre-built bundle covers this request. | ||
bundle = find_pullbundle(repo, proto, opts, clheads, heads, common) | ||||
if bundle: | ||||
|
r43346 | return wireprototypes.streamres( | ||
gen=util.filechunkiter(bundle), prefer_uncompressed=True | ||||
) | ||||
|
r37803 | |||
|
r43347 | if repo.ui.configbool(b'server', b'disablefullbundle'): | ||
|
r37803 | # Check to see if this is a full clone. | ||
|
r43347 | changegroup = opts.get(b'cg', True) | ||
|
r37803 | if changegroup and not common and clheads == heads: | ||
raise error.Abort( | ||||
|
r43347 | _(b'server has pull-based clones disabled'), | ||
hint=_(b'remove --pull if specified or upgrade Mercurial'), | ||||
|
r43346 | ) | ||
|
r37803 | |||
|
r43346 | info, chunks = exchange.getbundlechunks( | ||
|
r43347 | repo, b'serve', **pycompat.strkwargs(opts) | ||
|
r43346 | ) | ||
|
r43347 | prefercompressed = info.get(b'prefercompressed', True) | ||
|
r37803 | except error.Abort as exc: | ||
# cleanly forward Abort error to the client | ||||
|
r43347 | if not exchange.bundle2requested(opts.get(b'bundlecaps')): | ||
if proto.name == b'http-v1': | ||||
|
r46274 | return wireprototypes.ooberror(exc.message + b'\n') | ||
|
r43346 | raise # cannot do better for bundle1 + ssh | ||
|
r37803 | # bundle2 request expect a bundle2 reply | ||
bundler = bundle2.bundle20(repo.ui) | ||||
|
r46274 | manargs = [(b'message', exc.message)] | ||
|
r37803 | advargs = [] | ||
if exc.hint is not None: | ||||
|
r43347 | advargs.append((b'hint', exc.hint)) | ||
bundler.addpart(bundle2.bundlepart(b'error:abort', manargs, advargs)) | ||||
|
r37803 | chunks = bundler.getchunks() | ||
prefercompressed = False | ||||
return wireprototypes.streamres( | ||||
|
r43346 | gen=chunks, prefer_uncompressed=not prefercompressed | ||
) | ||||
|
r37803 | |||
|
r43347 | @wireprotocommand(b'heads', permission=b'pull') | ||
|
r37803 | def heads(repo, proto): | ||
h = repo.heads() | ||||
|
r43347 | return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n') | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'hello', permission=b'pull') | ||
|
r37803 | def hello(repo, proto): | ||
"""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. | ||||
Currently, the only one defined is ``capabilities``, which consists of a | ||||
line of space separated tokens describing server abilities: | ||||
capabilities: <token0> <token1> <token2> | ||||
""" | ||||
caps = capabilities(repo, proto).data | ||||
|
r43347 | return wireprototypes.bytesresponse(b'capabilities: %s\n' % caps) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'listkeys', b'namespace', permission=b'pull') | ||
|
r37803 | def listkeys(repo, proto, namespace): | ||
d = sorted(repo.listkeys(encoding.tolocal(namespace)).items()) | ||||
return wireprototypes.bytesresponse(pushkeymod.encodekeys(d)) | ||||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'lookup', b'key', permission=b'pull') | ||
|
r37803 | def lookup(repo, proto, key): | ||
try: | ||||
k = encoding.tolocal(key) | ||||
n = repo.lookup(k) | ||||
r = hex(n) | ||||
success = 1 | ||||
except Exception as inst: | ||||
r = stringutil.forcebytestr(inst) | ||||
success = 0 | ||||
|
r43347 | return wireprototypes.bytesresponse(b'%d %s\n' % (success, r)) | ||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'known', b'nodes *', permission=b'pull') | ||
|
r37803 | def known(repo, proto, nodes, others): | ||
|
r43347 | v = b''.join( | ||
b and b'1' or b'0' for b in repo.known(wireprototypes.decodelist(nodes)) | ||||
|
r43346 | ) | ||
|
r37803 | return wireprototypes.bytesresponse(v) | ||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'protocaps', b'caps', permission=b'pull') | ||
|
r37803 | def protocaps(repo, proto, caps): | ||
if proto.name == wireprototypes.SSHV1: | ||||
|
r43347 | proto._protocaps = set(caps.split(b' ')) | ||
return wireprototypes.bytesresponse(b'OK') | ||||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'pushkey', b'namespace key old new', permission=b'push') | ||
|
r37803 | def pushkey(repo, proto, namespace, key, old, new): | ||
# compatibility with pre-1.8 clients which were accidentally | ||||
# sending raw binary nodes rather than utf-8-encoded hex | ||||
if len(new) == 20 and stringutil.escapestr(new) != new: | ||||
# looks like it could be a binary node | ||||
try: | ||||
new.decode('utf-8') | ||||
|
r43346 | new = encoding.tolocal(new) # but cleanly decodes as UTF-8 | ||
|
r37803 | except UnicodeDecodeError: | ||
|
r43346 | pass # binary, leave unmodified | ||
|
r37803 | else: | ||
|
r43346 | new = encoding.tolocal(new) # normal path | ||
|
r37803 | |||
with proto.mayberedirectstdio() as output: | ||||
|
r43346 | r = ( | ||
repo.pushkey( | ||||
encoding.tolocal(namespace), | ||||
encoding.tolocal(key), | ||||
encoding.tolocal(old), | ||||
new, | ||||
) | ||||
or False | ||||
) | ||||
|
r37803 | |||
|
r43347 | output = output.getvalue() if output else b'' | ||
return wireprototypes.bytesresponse(b'%d\n%s' % (int(r), output)) | ||||
|
r37803 | |||
|
r43346 | |||
|
r43347 | @wireprotocommand(b'stream_out', permission=b'pull') | ||
|
r37803 | def stream(repo, proto): | ||
'''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. | ||||
''' | ||||
|
r43346 | return wireprototypes.streamreslegacy(streamclone.generatev1wireproto(repo)) | ||
|
r37803 | |||
|
r43347 | @wireprotocommand(b'unbundle', b'heads', permission=b'push') | ||
|
r37803 | def unbundle(repo, proto, heads): | ||
their_heads = wireprototypes.decodelist(heads) | ||||
with proto.mayberedirectstdio() as output: | ||||
try: | ||||
|
r43347 | exchange.check_heads(repo, their_heads, b'preparing changes') | ||
|
r37803 | cleanup = lambda: None | ||
try: | ||||
payload = proto.getpayload() | ||||
|
r43347 | if repo.ui.configbool(b'server', b'streamunbundle'): | ||
|
r43346 | |||
|
r37803 | def cleanup(): | ||
# Ensure that the full payload is consumed, so | ||||
# that the connection doesn't contain trailing garbage. | ||||
for p in payload: | ||||
pass | ||||
|
r43346 | |||
|
r37803 | fp = util.chunkbuffer(payload) | ||
else: | ||||
# write bundle data to temporary file as it can be big | ||||
fp, tempname = None, None | ||||
|
r43346 | |||
|
r37803 | def cleanup(): | ||
if fp: | ||||
fp.close() | ||||
if tempname: | ||||
os.unlink(tempname) | ||||
|
r43346 | |||
|
r43347 | fd, tempname = pycompat.mkstemp(prefix=b'hg-unbundle-') | ||
|
r43346 | repo.ui.debug( | ||
|
r43347 | b'redirecting incoming bundle to %s\n' % tempname | ||
|
r43346 | ) | ||
|
r43347 | fp = os.fdopen(fd, pycompat.sysstr(b'wb+')) | ||
|
r37803 | for p in payload: | ||
fp.write(p) | ||||
fp.seek(0) | ||||
gen = exchange.readbundle(repo.ui, fp, None) | ||||
|
r43346 | if isinstance( | ||
gen, changegroupmod.cg1unpacker | ||||
|
r43347 | ) and not bundle1allowed(repo, b'push'): | ||
if proto.name == b'http-v1': | ||||
|
r37803 | # 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 wireprototypes.ooberror(bundle2required) | ||||
|
r43346 | raise error.Abort( | ||
bundle2requiredmain, hint=bundle2requiredhint | ||||
) | ||||
|
r37803 | |||
|
r43346 | r = exchange.unbundle( | ||
|
r43347 | repo, gen, their_heads, b'serve', proto.client() | ||
|
r43346 | ) | ||
|
r43347 | if util.safehasattr(r, b'addpart'): | ||
|
r37803 | # The return looks streamable, we are in the bundle2 case | ||
# and should return a stream. | ||||
return wireprototypes.streamreslegacy(gen=r.getchunks()) | ||||
return wireprototypes.pushres( | ||||
|
r43347 | r, output.getvalue() if output else b'' | ||
|
r43346 | ) | ||
|
r37803 | |||
finally: | ||||
cleanup() | ||||
except (error.BundleValueError, error.Abort, error.PushRaced) as exc: | ||||
# handle non-bundle2 case first | ||||
if not getattr(exc, 'duringunbundle2', False): | ||||
try: | ||||
raise | ||||
|
r44122 | except error.Abort as exc: | ||
|
r37803 | # The old code we moved used procutil.stderr directly. | ||
# We did not change it to minimise code change. | ||||
# This need to be moved to something proper. | ||||
# Feel free to do it. | ||||
|
r46497 | procutil.stderr.write(exc.format()) | ||
|
r37803 | procutil.stderr.flush() | ||
return wireprototypes.pushres( | ||||
|
r43347 | 0, output.getvalue() if output else b'' | ||
|
r43346 | ) | ||
|
r37803 | except error.PushRaced: | ||
return wireprototypes.pusherr( | ||||
pycompat.bytestr(exc), | ||||
|
r43347 | output.getvalue() if output else b'', | ||
|
r43346 | ) | ||
|
r37803 | |||
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) | ||||
|
r43346 | if ( | ||
remotecaps is not None | ||||
|
r43347 | and b'pushkey' not in remotecaps.get(b'error', ()) | ||
|
r43346 | ): | ||
|
r37803 | # no support remote side, fallback to Abort handler. | ||
raise | ||||
|
r43347 | part = bundler.newpart(b'error:pushkey') | ||
part.addparam(b'in-reply-to', exc.partid) | ||||
|
r37803 | if exc.namespace is not None: | ||
|
r43346 | part.addparam( | ||
|
r43347 | b'namespace', exc.namespace, mandatory=False | ||
|
r43346 | ) | ||
|
r37803 | if exc.key is not None: | ||
|
r43347 | part.addparam(b'key', exc.key, mandatory=False) | ||
|
r37803 | if exc.new is not None: | ||
|
r43347 | part.addparam(b'new', exc.new, mandatory=False) | ||
|
r37803 | if exc.old is not None: | ||
|
r43347 | part.addparam(b'old', exc.old, mandatory=False) | ||
|
r37803 | if exc.ret is not None: | ||
|
r43347 | part.addparam(b'ret', exc.ret, mandatory=False) | ||
|
r37803 | except error.BundleValueError as exc: | ||
|
r43347 | errpart = bundler.newpart(b'error:unsupportedcontent') | ||
|
r37803 | if exc.parttype is not None: | ||
|
r43347 | errpart.addparam(b'parttype', exc.parttype) | ||
|
r37803 | if exc.params: | ||
|
r43347 | errpart.addparam(b'params', b'\0'.join(exc.params)) | ||
|
r37803 | except error.Abort as exc: | ||
|
r46274 | manargs = [(b'message', exc.message)] | ||
|
r37803 | advargs = [] | ||
if exc.hint is not None: | ||||
|
r43347 | advargs.append((b'hint', exc.hint)) | ||
|
r43346 | bundler.addpart( | ||
|
r43347 | bundle2.bundlepart(b'error:abort', manargs, advargs) | ||
|
r43346 | ) | ||
|
r37803 | except error.PushRaced as exc: | ||
|
r43346 | bundler.newpart( | ||
|
r43347 | b'error:pushraced', | ||
[(b'message', stringutil.forcebytestr(exc))], | ||||
|
r43346 | ) | ||
|
r37803 | return wireprototypes.streamreslegacy(gen=bundler.getchunks()) | ||