##// END OF EJS Templates
wireproto: move version 2 commands dict to wireprotov2server...
Gregory Szorc -
r37802:ee0d5e9d default
parent child Browse files
Show More
@@ -1,671 +1,667
1 # wireproto.py - generic wire protocol support functions
1 # wireproto.py - generic wire protocol support functions
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import tempfile
11 import tempfile
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 hex,
15 hex,
16 nullid,
16 nullid,
17 )
17 )
18
18
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 changegroup as changegroupmod,
21 changegroup as changegroupmod,
22 discovery,
22 discovery,
23 encoding,
23 encoding,
24 error,
24 error,
25 exchange,
25 exchange,
26 pushkey as pushkeymod,
26 pushkey as pushkeymod,
27 pycompat,
27 pycompat,
28 streamclone,
28 streamclone,
29 util,
29 util,
30 wireprototypes,
30 wireprototypes,
31 )
31 )
32
32
33 from .utils import (
33 from .utils import (
34 procutil,
34 procutil,
35 stringutil,
35 stringutil,
36 )
36 )
37
37
38 urlerr = util.urlerr
38 urlerr = util.urlerr
39 urlreq = util.urlreq
39 urlreq = util.urlreq
40
40
41 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
41 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
42 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
42 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
43 'IncompatibleClient')
43 'IncompatibleClient')
44 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
44 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
45
45
46 def clientcompressionsupport(proto):
46 def clientcompressionsupport(proto):
47 """Returns a list of compression methods supported by the client.
47 """Returns a list of compression methods supported by the client.
48
48
49 Returns a list of the compression methods supported by the client
49 Returns a list of the compression methods supported by the client
50 according to the protocol capabilities. If no such capability has
50 according to the protocol capabilities. If no such capability has
51 been announced, fallback to the default of zlib and uncompressed.
51 been announced, fallback to the default of zlib and uncompressed.
52 """
52 """
53 for cap in proto.getprotocaps():
53 for cap in proto.getprotocaps():
54 if cap.startswith('comp='):
54 if cap.startswith('comp='):
55 return cap[5:].split(',')
55 return cap[5:].split(',')
56 return ['zlib', 'none']
56 return ['zlib', 'none']
57
57
58 # wire protocol command can either return a string or one of these classes.
58 # wire protocol command can either return a string or one of these classes.
59
59
60 def getdispatchrepo(repo, proto, command):
60 def getdispatchrepo(repo, proto, command):
61 """Obtain the repo used for processing wire protocol commands.
61 """Obtain the repo used for processing wire protocol commands.
62
62
63 The intent of this function is to serve as a monkeypatch point for
63 The intent of this function is to serve as a monkeypatch point for
64 extensions that need commands to operate on different repo views under
64 extensions that need commands to operate on different repo views under
65 specialized circumstances.
65 specialized circumstances.
66 """
66 """
67 return repo.filtered('served')
67 return repo.filtered('served')
68
68
69 def dispatch(repo, proto, command):
69 def dispatch(repo, proto, command):
70 repo = getdispatchrepo(repo, proto, command)
70 repo = getdispatchrepo(repo, proto, command)
71
71
72 func, spec = commands[command]
72 func, spec = commands[command]
73 args = proto.getargs(spec)
73 args = proto.getargs(spec)
74
74
75 return func(repo, proto, *args)
75 return func(repo, proto, *args)
76
76
77 def options(cmd, keys, others):
77 def options(cmd, keys, others):
78 opts = {}
78 opts = {}
79 for k in keys:
79 for k in keys:
80 if k in others:
80 if k in others:
81 opts[k] = others[k]
81 opts[k] = others[k]
82 del others[k]
82 del others[k]
83 if others:
83 if others:
84 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
84 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
85 % (cmd, ",".join(others)))
85 % (cmd, ",".join(others)))
86 return opts
86 return opts
87
87
88 def bundle1allowed(repo, action):
88 def bundle1allowed(repo, action):
89 """Whether a bundle1 operation is allowed from the server.
89 """Whether a bundle1 operation is allowed from the server.
90
90
91 Priority is:
91 Priority is:
92
92
93 1. server.bundle1gd.<action> (if generaldelta active)
93 1. server.bundle1gd.<action> (if generaldelta active)
94 2. server.bundle1.<action>
94 2. server.bundle1.<action>
95 3. server.bundle1gd (if generaldelta active)
95 3. server.bundle1gd (if generaldelta active)
96 4. server.bundle1
96 4. server.bundle1
97 """
97 """
98 ui = repo.ui
98 ui = repo.ui
99 gd = 'generaldelta' in repo.requirements
99 gd = 'generaldelta' in repo.requirements
100
100
101 if gd:
101 if gd:
102 v = ui.configbool('server', 'bundle1gd.%s' % action)
102 v = ui.configbool('server', 'bundle1gd.%s' % action)
103 if v is not None:
103 if v is not None:
104 return v
104 return v
105
105
106 v = ui.configbool('server', 'bundle1.%s' % action)
106 v = ui.configbool('server', 'bundle1.%s' % action)
107 if v is not None:
107 if v is not None:
108 return v
108 return v
109
109
110 if gd:
110 if gd:
111 v = ui.configbool('server', 'bundle1gd')
111 v = ui.configbool('server', 'bundle1gd')
112 if v is not None:
112 if v is not None:
113 return v
113 return v
114
114
115 return ui.configbool('server', 'bundle1')
115 return ui.configbool('server', 'bundle1')
116
116
117 # For version 1 transports.
118 commands = wireprototypes.commanddict()
117 commands = wireprototypes.commanddict()
119
118
120 # For version 2 transports.
121 commandsv2 = wireprototypes.commanddict()
122
123 def wireprotocommand(name, args=None, permission='push'):
119 def wireprotocommand(name, args=None, permission='push'):
124 """Decorator to declare a wire protocol command.
120 """Decorator to declare a wire protocol command.
125
121
126 ``name`` is the name of the wire protocol command being provided.
122 ``name`` is the name of the wire protocol command being provided.
127
123
128 ``args`` defines the named arguments accepted by the command. It is
124 ``args`` defines the named arguments accepted by the command. It is
129 a space-delimited list of argument names. ``*`` denotes a special value
125 a space-delimited list of argument names. ``*`` denotes a special value
130 that says to accept all named arguments.
126 that says to accept all named arguments.
131
127
132 ``permission`` defines the permission type needed to run this command.
128 ``permission`` defines the permission type needed to run this command.
133 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
129 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
134 respectively. Default is to assume command requires ``push`` permissions
130 respectively. Default is to assume command requires ``push`` permissions
135 because otherwise commands not declaring their permissions could modify
131 because otherwise commands not declaring their permissions could modify
136 a repository that is supposed to be read-only.
132 a repository that is supposed to be read-only.
137 """
133 """
138 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
134 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
139 if v['version'] == 1}
135 if v['version'] == 1}
140
136
141 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
137 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
142 # SSHv2.
138 # SSHv2.
143 # TODO undo this hack when SSH is using the unified frame protocol.
139 # TODO undo this hack when SSH is using the unified frame protocol.
144 if name == b'batch':
140 if name == b'batch':
145 transports.add(wireprototypes.SSHV2)
141 transports.add(wireprototypes.SSHV2)
146
142
147 if permission not in ('push', 'pull'):
143 if permission not in ('push', 'pull'):
148 raise error.ProgrammingError('invalid wire protocol permission; '
144 raise error.ProgrammingError('invalid wire protocol permission; '
149 'got %s; expected "push" or "pull"' %
145 'got %s; expected "push" or "pull"' %
150 permission)
146 permission)
151
147
152 if args is None:
148 if args is None:
153 args = ''
149 args = ''
154
150
155 if not isinstance(args, bytes):
151 if not isinstance(args, bytes):
156 raise error.ProgrammingError('arguments for version 1 commands '
152 raise error.ProgrammingError('arguments for version 1 commands '
157 'must be declared as bytes')
153 'must be declared as bytes')
158
154
159 def register(func):
155 def register(func):
160 if name in commands:
156 if name in commands:
161 raise error.ProgrammingError('%s command already registered '
157 raise error.ProgrammingError('%s command already registered '
162 'for version 1' % name)
158 'for version 1' % name)
163 commands[name] = wireprototypes.commandentry(
159 commands[name] = wireprototypes.commandentry(
164 func, args=args, transports=transports, permission=permission)
160 func, args=args, transports=transports, permission=permission)
165
161
166 return func
162 return func
167 return register
163 return register
168
164
169 # TODO define a more appropriate permissions type to use for this.
165 # TODO define a more appropriate permissions type to use for this.
170 @wireprotocommand('batch', 'cmds *', permission='pull')
166 @wireprotocommand('batch', 'cmds *', permission='pull')
171 def batch(repo, proto, cmds, others):
167 def batch(repo, proto, cmds, others):
172 unescapearg = wireprototypes.unescapebatcharg
168 unescapearg = wireprototypes.unescapebatcharg
173 repo = repo.filtered("served")
169 repo = repo.filtered("served")
174 res = []
170 res = []
175 for pair in cmds.split(';'):
171 for pair in cmds.split(';'):
176 op, args = pair.split(' ', 1)
172 op, args = pair.split(' ', 1)
177 vals = {}
173 vals = {}
178 for a in args.split(','):
174 for a in args.split(','):
179 if a:
175 if a:
180 n, v = a.split('=')
176 n, v = a.split('=')
181 vals[unescapearg(n)] = unescapearg(v)
177 vals[unescapearg(n)] = unescapearg(v)
182 func, spec = commands[op]
178 func, spec = commands[op]
183
179
184 # Validate that client has permissions to perform this command.
180 # Validate that client has permissions to perform this command.
185 perm = commands[op].permission
181 perm = commands[op].permission
186 assert perm in ('push', 'pull')
182 assert perm in ('push', 'pull')
187 proto.checkperm(perm)
183 proto.checkperm(perm)
188
184
189 if spec:
185 if spec:
190 keys = spec.split()
186 keys = spec.split()
191 data = {}
187 data = {}
192 for k in keys:
188 for k in keys:
193 if k == '*':
189 if k == '*':
194 star = {}
190 star = {}
195 for key in vals.keys():
191 for key in vals.keys():
196 if key not in keys:
192 if key not in keys:
197 star[key] = vals[key]
193 star[key] = vals[key]
198 data['*'] = star
194 data['*'] = star
199 else:
195 else:
200 data[k] = vals[k]
196 data[k] = vals[k]
201 result = func(repo, proto, *[data[k] for k in keys])
197 result = func(repo, proto, *[data[k] for k in keys])
202 else:
198 else:
203 result = func(repo, proto)
199 result = func(repo, proto)
204 if isinstance(result, wireprototypes.ooberror):
200 if isinstance(result, wireprototypes.ooberror):
205 return result
201 return result
206
202
207 # For now, all batchable commands must return bytesresponse or
203 # For now, all batchable commands must return bytesresponse or
208 # raw bytes (for backwards compatibility).
204 # raw bytes (for backwards compatibility).
209 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
205 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
210 if isinstance(result, wireprototypes.bytesresponse):
206 if isinstance(result, wireprototypes.bytesresponse):
211 result = result.data
207 result = result.data
212 res.append(wireprototypes.escapebatcharg(result))
208 res.append(wireprototypes.escapebatcharg(result))
213
209
214 return wireprototypes.bytesresponse(';'.join(res))
210 return wireprototypes.bytesresponse(';'.join(res))
215
211
216 @wireprotocommand('between', 'pairs', permission='pull')
212 @wireprotocommand('between', 'pairs', permission='pull')
217 def between(repo, proto, pairs):
213 def between(repo, proto, pairs):
218 pairs = [wireprototypes.decodelist(p, '-') for p in pairs.split(" ")]
214 pairs = [wireprototypes.decodelist(p, '-') for p in pairs.split(" ")]
219 r = []
215 r = []
220 for b in repo.between(pairs):
216 for b in repo.between(pairs):
221 r.append(wireprototypes.encodelist(b) + "\n")
217 r.append(wireprototypes.encodelist(b) + "\n")
222
218
223 return wireprototypes.bytesresponse(''.join(r))
219 return wireprototypes.bytesresponse(''.join(r))
224
220
225 @wireprotocommand('branchmap', permission='pull')
221 @wireprotocommand('branchmap', permission='pull')
226 def branchmap(repo, proto):
222 def branchmap(repo, proto):
227 branchmap = repo.branchmap()
223 branchmap = repo.branchmap()
228 heads = []
224 heads = []
229 for branch, nodes in branchmap.iteritems():
225 for branch, nodes in branchmap.iteritems():
230 branchname = urlreq.quote(encoding.fromlocal(branch))
226 branchname = urlreq.quote(encoding.fromlocal(branch))
231 branchnodes = wireprototypes.encodelist(nodes)
227 branchnodes = wireprototypes.encodelist(nodes)
232 heads.append('%s %s' % (branchname, branchnodes))
228 heads.append('%s %s' % (branchname, branchnodes))
233
229
234 return wireprototypes.bytesresponse('\n'.join(heads))
230 return wireprototypes.bytesresponse('\n'.join(heads))
235
231
236 @wireprotocommand('branches', 'nodes', permission='pull')
232 @wireprotocommand('branches', 'nodes', permission='pull')
237 def branches(repo, proto, nodes):
233 def branches(repo, proto, nodes):
238 nodes = wireprototypes.decodelist(nodes)
234 nodes = wireprototypes.decodelist(nodes)
239 r = []
235 r = []
240 for b in repo.branches(nodes):
236 for b in repo.branches(nodes):
241 r.append(wireprototypes.encodelist(b) + "\n")
237 r.append(wireprototypes.encodelist(b) + "\n")
242
238
243 return wireprototypes.bytesresponse(''.join(r))
239 return wireprototypes.bytesresponse(''.join(r))
244
240
245 @wireprotocommand('clonebundles', '', permission='pull')
241 @wireprotocommand('clonebundles', '', permission='pull')
246 def clonebundles(repo, proto):
242 def clonebundles(repo, proto):
247 """Server command for returning info for available bundles to seed clones.
243 """Server command for returning info for available bundles to seed clones.
248
244
249 Clients will parse this response and determine what bundle to fetch.
245 Clients will parse this response and determine what bundle to fetch.
250
246
251 Extensions may wrap this command to filter or dynamically emit data
247 Extensions may wrap this command to filter or dynamically emit data
252 depending on the request. e.g. you could advertise URLs for the closest
248 depending on the request. e.g. you could advertise URLs for the closest
253 data center given the client's IP address.
249 data center given the client's IP address.
254 """
250 """
255 return wireprototypes.bytesresponse(
251 return wireprototypes.bytesresponse(
256 repo.vfs.tryread('clonebundles.manifest'))
252 repo.vfs.tryread('clonebundles.manifest'))
257
253
258 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
254 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
259 'known', 'getbundle', 'unbundlehash']
255 'known', 'getbundle', 'unbundlehash']
260
256
261 def _capabilities(repo, proto):
257 def _capabilities(repo, proto):
262 """return a list of capabilities for a repo
258 """return a list of capabilities for a repo
263
259
264 This function exists to allow extensions to easily wrap capabilities
260 This function exists to allow extensions to easily wrap capabilities
265 computation
261 computation
266
262
267 - returns a lists: easy to alter
263 - returns a lists: easy to alter
268 - change done here will be propagated to both `capabilities` and `hello`
264 - change done here will be propagated to both `capabilities` and `hello`
269 command without any other action needed.
265 command without any other action needed.
270 """
266 """
271 # copy to prevent modification of the global list
267 # copy to prevent modification of the global list
272 caps = list(wireprotocaps)
268 caps = list(wireprotocaps)
273
269
274 # Command of same name as capability isn't exposed to version 1 of
270 # Command of same name as capability isn't exposed to version 1 of
275 # transports. So conditionally add it.
271 # transports. So conditionally add it.
276 if commands.commandavailable('changegroupsubset', proto):
272 if commands.commandavailable('changegroupsubset', proto):
277 caps.append('changegroupsubset')
273 caps.append('changegroupsubset')
278
274
279 if streamclone.allowservergeneration(repo):
275 if streamclone.allowservergeneration(repo):
280 if repo.ui.configbool('server', 'preferuncompressed'):
276 if repo.ui.configbool('server', 'preferuncompressed'):
281 caps.append('stream-preferred')
277 caps.append('stream-preferred')
282 requiredformats = repo.requirements & repo.supportedformats
278 requiredformats = repo.requirements & repo.supportedformats
283 # if our local revlogs are just revlogv1, add 'stream' cap
279 # if our local revlogs are just revlogv1, add 'stream' cap
284 if not requiredformats - {'revlogv1'}:
280 if not requiredformats - {'revlogv1'}:
285 caps.append('stream')
281 caps.append('stream')
286 # otherwise, add 'streamreqs' detailing our local revlog format
282 # otherwise, add 'streamreqs' detailing our local revlog format
287 else:
283 else:
288 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
284 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
289 if repo.ui.configbool('experimental', 'bundle2-advertise'):
285 if repo.ui.configbool('experimental', 'bundle2-advertise'):
290 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
286 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
291 caps.append('bundle2=' + urlreq.quote(capsblob))
287 caps.append('bundle2=' + urlreq.quote(capsblob))
292 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
288 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
293
289
294 return proto.addcapabilities(repo, caps)
290 return proto.addcapabilities(repo, caps)
295
291
296 # If you are writing an extension and consider wrapping this function. Wrap
292 # If you are writing an extension and consider wrapping this function. Wrap
297 # `_capabilities` instead.
293 # `_capabilities` instead.
298 @wireprotocommand('capabilities', permission='pull')
294 @wireprotocommand('capabilities', permission='pull')
299 def capabilities(repo, proto):
295 def capabilities(repo, proto):
300 caps = _capabilities(repo, proto)
296 caps = _capabilities(repo, proto)
301 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
297 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
302
298
303 @wireprotocommand('changegroup', 'roots', permission='pull')
299 @wireprotocommand('changegroup', 'roots', permission='pull')
304 def changegroup(repo, proto, roots):
300 def changegroup(repo, proto, roots):
305 nodes = wireprototypes.decodelist(roots)
301 nodes = wireprototypes.decodelist(roots)
306 outgoing = discovery.outgoing(repo, missingroots=nodes,
302 outgoing = discovery.outgoing(repo, missingroots=nodes,
307 missingheads=repo.heads())
303 missingheads=repo.heads())
308 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
304 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
309 gen = iter(lambda: cg.read(32768), '')
305 gen = iter(lambda: cg.read(32768), '')
310 return wireprototypes.streamres(gen=gen)
306 return wireprototypes.streamres(gen=gen)
311
307
312 @wireprotocommand('changegroupsubset', 'bases heads',
308 @wireprotocommand('changegroupsubset', 'bases heads',
313 permission='pull')
309 permission='pull')
314 def changegroupsubset(repo, proto, bases, heads):
310 def changegroupsubset(repo, proto, bases, heads):
315 bases = wireprototypes.decodelist(bases)
311 bases = wireprototypes.decodelist(bases)
316 heads = wireprototypes.decodelist(heads)
312 heads = wireprototypes.decodelist(heads)
317 outgoing = discovery.outgoing(repo, missingroots=bases,
313 outgoing = discovery.outgoing(repo, missingroots=bases,
318 missingheads=heads)
314 missingheads=heads)
319 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
315 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
320 gen = iter(lambda: cg.read(32768), '')
316 gen = iter(lambda: cg.read(32768), '')
321 return wireprototypes.streamres(gen=gen)
317 return wireprototypes.streamres(gen=gen)
322
318
323 @wireprotocommand('debugwireargs', 'one two *',
319 @wireprotocommand('debugwireargs', 'one two *',
324 permission='pull')
320 permission='pull')
325 def debugwireargs(repo, proto, one, two, others):
321 def debugwireargs(repo, proto, one, two, others):
326 # only accept optional args from the known set
322 # only accept optional args from the known set
327 opts = options('debugwireargs', ['three', 'four'], others)
323 opts = options('debugwireargs', ['three', 'four'], others)
328 return wireprototypes.bytesresponse(repo.debugwireargs(
324 return wireprototypes.bytesresponse(repo.debugwireargs(
329 one, two, **pycompat.strkwargs(opts)))
325 one, two, **pycompat.strkwargs(opts)))
330
326
331 def find_pullbundle(repo, proto, opts, clheads, heads, common):
327 def find_pullbundle(repo, proto, opts, clheads, heads, common):
332 """Return a file object for the first matching pullbundle.
328 """Return a file object for the first matching pullbundle.
333
329
334 Pullbundles are specified in .hg/pullbundles.manifest similar to
330 Pullbundles are specified in .hg/pullbundles.manifest similar to
335 clonebundles.
331 clonebundles.
336 For each entry, the bundle specification is checked for compatibility:
332 For each entry, the bundle specification is checked for compatibility:
337 - Client features vs the BUNDLESPEC.
333 - Client features vs the BUNDLESPEC.
338 - Revisions shared with the clients vs base revisions of the bundle.
334 - Revisions shared with the clients vs base revisions of the bundle.
339 A bundle can be applied only if all its base revisions are known by
335 A bundle can be applied only if all its base revisions are known by
340 the client.
336 the client.
341 - At least one leaf of the bundle's DAG is missing on the client.
337 - At least one leaf of the bundle's DAG is missing on the client.
342 - Every leaf of the bundle's DAG is part of node set the client wants.
338 - Every leaf of the bundle's DAG is part of node set the client wants.
343 E.g. do not send a bundle of all changes if the client wants only
339 E.g. do not send a bundle of all changes if the client wants only
344 one specific branch of many.
340 one specific branch of many.
345 """
341 """
346 def decodehexstring(s):
342 def decodehexstring(s):
347 return set([h.decode('hex') for h in s.split(';')])
343 return set([h.decode('hex') for h in s.split(';')])
348
344
349 manifest = repo.vfs.tryread('pullbundles.manifest')
345 manifest = repo.vfs.tryread('pullbundles.manifest')
350 if not manifest:
346 if not manifest:
351 return None
347 return None
352 res = exchange.parseclonebundlesmanifest(repo, manifest)
348 res = exchange.parseclonebundlesmanifest(repo, manifest)
353 res = exchange.filterclonebundleentries(repo, res)
349 res = exchange.filterclonebundleentries(repo, res)
354 if not res:
350 if not res:
355 return None
351 return None
356 cl = repo.changelog
352 cl = repo.changelog
357 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
353 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
358 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
354 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
359 compformats = clientcompressionsupport(proto)
355 compformats = clientcompressionsupport(proto)
360 for entry in res:
356 for entry in res:
361 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
357 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
362 continue
358 continue
363 # No test yet for VERSION, since V2 is supported by any client
359 # No test yet for VERSION, since V2 is supported by any client
364 # that advertises partial pulls
360 # that advertises partial pulls
365 if 'heads' in entry:
361 if 'heads' in entry:
366 try:
362 try:
367 bundle_heads = decodehexstring(entry['heads'])
363 bundle_heads = decodehexstring(entry['heads'])
368 except TypeError:
364 except TypeError:
369 # Bad heads entry
365 # Bad heads entry
370 continue
366 continue
371 if bundle_heads.issubset(common):
367 if bundle_heads.issubset(common):
372 continue # Nothing new
368 continue # Nothing new
373 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
369 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
374 continue # Still nothing new
370 continue # Still nothing new
375 if any(cl.rev(rev) not in heads_anc and
371 if any(cl.rev(rev) not in heads_anc and
376 cl.rev(rev) not in common_anc for rev in bundle_heads):
372 cl.rev(rev) not in common_anc for rev in bundle_heads):
377 continue
373 continue
378 if 'bases' in entry:
374 if 'bases' in entry:
379 try:
375 try:
380 bundle_bases = decodehexstring(entry['bases'])
376 bundle_bases = decodehexstring(entry['bases'])
381 except TypeError:
377 except TypeError:
382 # Bad bases entry
378 # Bad bases entry
383 continue
379 continue
384 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
380 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
385 continue
381 continue
386 path = entry['URL']
382 path = entry['URL']
387 repo.ui.debug('sending pullbundle "%s"\n' % path)
383 repo.ui.debug('sending pullbundle "%s"\n' % path)
388 try:
384 try:
389 return repo.vfs.open(path)
385 return repo.vfs.open(path)
390 except IOError:
386 except IOError:
391 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
387 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
392 continue
388 continue
393 return None
389 return None
394
390
395 @wireprotocommand('getbundle', '*', permission='pull')
391 @wireprotocommand('getbundle', '*', permission='pull')
396 def getbundle(repo, proto, others):
392 def getbundle(repo, proto, others):
397 opts = options('getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
393 opts = options('getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
398 others)
394 others)
399 for k, v in opts.iteritems():
395 for k, v in opts.iteritems():
400 keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
396 keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
401 if keytype == 'nodes':
397 if keytype == 'nodes':
402 opts[k] = wireprototypes.decodelist(v)
398 opts[k] = wireprototypes.decodelist(v)
403 elif keytype == 'csv':
399 elif keytype == 'csv':
404 opts[k] = list(v.split(','))
400 opts[k] = list(v.split(','))
405 elif keytype == 'scsv':
401 elif keytype == 'scsv':
406 opts[k] = set(v.split(','))
402 opts[k] = set(v.split(','))
407 elif keytype == 'boolean':
403 elif keytype == 'boolean':
408 # Client should serialize False as '0', which is a non-empty string
404 # Client should serialize False as '0', which is a non-empty string
409 # so it evaluates as a True bool.
405 # so it evaluates as a True bool.
410 if v == '0':
406 if v == '0':
411 opts[k] = False
407 opts[k] = False
412 else:
408 else:
413 opts[k] = bool(v)
409 opts[k] = bool(v)
414 elif keytype != 'plain':
410 elif keytype != 'plain':
415 raise KeyError('unknown getbundle option type %s'
411 raise KeyError('unknown getbundle option type %s'
416 % keytype)
412 % keytype)
417
413
418 if not bundle1allowed(repo, 'pull'):
414 if not bundle1allowed(repo, 'pull'):
419 if not exchange.bundle2requested(opts.get('bundlecaps')):
415 if not exchange.bundle2requested(opts.get('bundlecaps')):
420 if proto.name == 'http-v1':
416 if proto.name == 'http-v1':
421 return wireprototypes.ooberror(bundle2required)
417 return wireprototypes.ooberror(bundle2required)
422 raise error.Abort(bundle2requiredmain,
418 raise error.Abort(bundle2requiredmain,
423 hint=bundle2requiredhint)
419 hint=bundle2requiredhint)
424
420
425 prefercompressed = True
421 prefercompressed = True
426
422
427 try:
423 try:
428 clheads = set(repo.changelog.heads())
424 clheads = set(repo.changelog.heads())
429 heads = set(opts.get('heads', set()))
425 heads = set(opts.get('heads', set()))
430 common = set(opts.get('common', set()))
426 common = set(opts.get('common', set()))
431 common.discard(nullid)
427 common.discard(nullid)
432 if (repo.ui.configbool('server', 'pullbundle') and
428 if (repo.ui.configbool('server', 'pullbundle') and
433 'partial-pull' in proto.getprotocaps()):
429 'partial-pull' in proto.getprotocaps()):
434 # Check if a pre-built bundle covers this request.
430 # Check if a pre-built bundle covers this request.
435 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
431 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
436 if bundle:
432 if bundle:
437 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
433 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
438 prefer_uncompressed=True)
434 prefer_uncompressed=True)
439
435
440 if repo.ui.configbool('server', 'disablefullbundle'):
436 if repo.ui.configbool('server', 'disablefullbundle'):
441 # Check to see if this is a full clone.
437 # Check to see if this is a full clone.
442 changegroup = opts.get('cg', True)
438 changegroup = opts.get('cg', True)
443 if changegroup and not common and clheads == heads:
439 if changegroup and not common and clheads == heads:
444 raise error.Abort(
440 raise error.Abort(
445 _('server has pull-based clones disabled'),
441 _('server has pull-based clones disabled'),
446 hint=_('remove --pull if specified or upgrade Mercurial'))
442 hint=_('remove --pull if specified or upgrade Mercurial'))
447
443
448 info, chunks = exchange.getbundlechunks(repo, 'serve',
444 info, chunks = exchange.getbundlechunks(repo, 'serve',
449 **pycompat.strkwargs(opts))
445 **pycompat.strkwargs(opts))
450 prefercompressed = info.get('prefercompressed', True)
446 prefercompressed = info.get('prefercompressed', True)
451 except error.Abort as exc:
447 except error.Abort as exc:
452 # cleanly forward Abort error to the client
448 # cleanly forward Abort error to the client
453 if not exchange.bundle2requested(opts.get('bundlecaps')):
449 if not exchange.bundle2requested(opts.get('bundlecaps')):
454 if proto.name == 'http-v1':
450 if proto.name == 'http-v1':
455 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
451 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
456 raise # cannot do better for bundle1 + ssh
452 raise # cannot do better for bundle1 + ssh
457 # bundle2 request expect a bundle2 reply
453 # bundle2 request expect a bundle2 reply
458 bundler = bundle2.bundle20(repo.ui)
454 bundler = bundle2.bundle20(repo.ui)
459 manargs = [('message', pycompat.bytestr(exc))]
455 manargs = [('message', pycompat.bytestr(exc))]
460 advargs = []
456 advargs = []
461 if exc.hint is not None:
457 if exc.hint is not None:
462 advargs.append(('hint', exc.hint))
458 advargs.append(('hint', exc.hint))
463 bundler.addpart(bundle2.bundlepart('error:abort',
459 bundler.addpart(bundle2.bundlepart('error:abort',
464 manargs, advargs))
460 manargs, advargs))
465 chunks = bundler.getchunks()
461 chunks = bundler.getchunks()
466 prefercompressed = False
462 prefercompressed = False
467
463
468 return wireprototypes.streamres(
464 return wireprototypes.streamres(
469 gen=chunks, prefer_uncompressed=not prefercompressed)
465 gen=chunks, prefer_uncompressed=not prefercompressed)
470
466
471 @wireprotocommand('heads', permission='pull')
467 @wireprotocommand('heads', permission='pull')
472 def heads(repo, proto):
468 def heads(repo, proto):
473 h = repo.heads()
469 h = repo.heads()
474 return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + '\n')
470 return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + '\n')
475
471
476 @wireprotocommand('hello', permission='pull')
472 @wireprotocommand('hello', permission='pull')
477 def hello(repo, proto):
473 def hello(repo, proto):
478 """Called as part of SSH handshake to obtain server info.
474 """Called as part of SSH handshake to obtain server info.
479
475
480 Returns a list of lines describing interesting things about the
476 Returns a list of lines describing interesting things about the
481 server, in an RFC822-like format.
477 server, in an RFC822-like format.
482
478
483 Currently, the only one defined is ``capabilities``, which consists of a
479 Currently, the only one defined is ``capabilities``, which consists of a
484 line of space separated tokens describing server abilities:
480 line of space separated tokens describing server abilities:
485
481
486 capabilities: <token0> <token1> <token2>
482 capabilities: <token0> <token1> <token2>
487 """
483 """
488 caps = capabilities(repo, proto).data
484 caps = capabilities(repo, proto).data
489 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
485 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
490
486
491 @wireprotocommand('listkeys', 'namespace', permission='pull')
487 @wireprotocommand('listkeys', 'namespace', permission='pull')
492 def listkeys(repo, proto, namespace):
488 def listkeys(repo, proto, namespace):
493 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
489 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
494 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
490 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
495
491
496 @wireprotocommand('lookup', 'key', permission='pull')
492 @wireprotocommand('lookup', 'key', permission='pull')
497 def lookup(repo, proto, key):
493 def lookup(repo, proto, key):
498 try:
494 try:
499 k = encoding.tolocal(key)
495 k = encoding.tolocal(key)
500 n = repo.lookup(k)
496 n = repo.lookup(k)
501 r = hex(n)
497 r = hex(n)
502 success = 1
498 success = 1
503 except Exception as inst:
499 except Exception as inst:
504 r = stringutil.forcebytestr(inst)
500 r = stringutil.forcebytestr(inst)
505 success = 0
501 success = 0
506 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
502 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
507
503
508 @wireprotocommand('known', 'nodes *', permission='pull')
504 @wireprotocommand('known', 'nodes *', permission='pull')
509 def known(repo, proto, nodes, others):
505 def known(repo, proto, nodes, others):
510 v = ''.join(b and '1' or '0'
506 v = ''.join(b and '1' or '0'
511 for b in repo.known(wireprototypes.decodelist(nodes)))
507 for b in repo.known(wireprototypes.decodelist(nodes)))
512 return wireprototypes.bytesresponse(v)
508 return wireprototypes.bytesresponse(v)
513
509
514 @wireprotocommand('protocaps', 'caps', permission='pull')
510 @wireprotocommand('protocaps', 'caps', permission='pull')
515 def protocaps(repo, proto, caps):
511 def protocaps(repo, proto, caps):
516 if proto.name == wireprototypes.SSHV1:
512 if proto.name == wireprototypes.SSHV1:
517 proto._protocaps = set(caps.split(' '))
513 proto._protocaps = set(caps.split(' '))
518 return wireprototypes.bytesresponse('OK')
514 return wireprototypes.bytesresponse('OK')
519
515
520 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
516 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
521 def pushkey(repo, proto, namespace, key, old, new):
517 def pushkey(repo, proto, namespace, key, old, new):
522 # compatibility with pre-1.8 clients which were accidentally
518 # compatibility with pre-1.8 clients which were accidentally
523 # sending raw binary nodes rather than utf-8-encoded hex
519 # sending raw binary nodes rather than utf-8-encoded hex
524 if len(new) == 20 and stringutil.escapestr(new) != new:
520 if len(new) == 20 and stringutil.escapestr(new) != new:
525 # looks like it could be a binary node
521 # looks like it could be a binary node
526 try:
522 try:
527 new.decode('utf-8')
523 new.decode('utf-8')
528 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
524 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
529 except UnicodeDecodeError:
525 except UnicodeDecodeError:
530 pass # binary, leave unmodified
526 pass # binary, leave unmodified
531 else:
527 else:
532 new = encoding.tolocal(new) # normal path
528 new = encoding.tolocal(new) # normal path
533
529
534 with proto.mayberedirectstdio() as output:
530 with proto.mayberedirectstdio() as output:
535 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
531 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
536 encoding.tolocal(old), new) or False
532 encoding.tolocal(old), new) or False
537
533
538 output = output.getvalue() if output else ''
534 output = output.getvalue() if output else ''
539 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
535 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
540
536
541 @wireprotocommand('stream_out', permission='pull')
537 @wireprotocommand('stream_out', permission='pull')
542 def stream(repo, proto):
538 def stream(repo, proto):
543 '''If the server supports streaming clone, it advertises the "stream"
539 '''If the server supports streaming clone, it advertises the "stream"
544 capability with a value representing the version and flags of the repo
540 capability with a value representing the version and flags of the repo
545 it is serving. Client checks to see if it understands the format.
541 it is serving. Client checks to see if it understands the format.
546 '''
542 '''
547 return wireprototypes.streamreslegacy(
543 return wireprototypes.streamreslegacy(
548 streamclone.generatev1wireproto(repo))
544 streamclone.generatev1wireproto(repo))
549
545
550 @wireprotocommand('unbundle', 'heads', permission='push')
546 @wireprotocommand('unbundle', 'heads', permission='push')
551 def unbundle(repo, proto, heads):
547 def unbundle(repo, proto, heads):
552 their_heads = wireprototypes.decodelist(heads)
548 their_heads = wireprototypes.decodelist(heads)
553
549
554 with proto.mayberedirectstdio() as output:
550 with proto.mayberedirectstdio() as output:
555 try:
551 try:
556 exchange.check_heads(repo, their_heads, 'preparing changes')
552 exchange.check_heads(repo, their_heads, 'preparing changes')
557 cleanup = lambda: None
553 cleanup = lambda: None
558 try:
554 try:
559 payload = proto.getpayload()
555 payload = proto.getpayload()
560 if repo.ui.configbool('server', 'streamunbundle'):
556 if repo.ui.configbool('server', 'streamunbundle'):
561 def cleanup():
557 def cleanup():
562 # Ensure that the full payload is consumed, so
558 # Ensure that the full payload is consumed, so
563 # that the connection doesn't contain trailing garbage.
559 # that the connection doesn't contain trailing garbage.
564 for p in payload:
560 for p in payload:
565 pass
561 pass
566 fp = util.chunkbuffer(payload)
562 fp = util.chunkbuffer(payload)
567 else:
563 else:
568 # write bundle data to temporary file as it can be big
564 # write bundle data to temporary file as it can be big
569 fp, tempname = None, None
565 fp, tempname = None, None
570 def cleanup():
566 def cleanup():
571 if fp:
567 if fp:
572 fp.close()
568 fp.close()
573 if tempname:
569 if tempname:
574 os.unlink(tempname)
570 os.unlink(tempname)
575 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
571 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
576 repo.ui.debug('redirecting incoming bundle to %s\n' %
572 repo.ui.debug('redirecting incoming bundle to %s\n' %
577 tempname)
573 tempname)
578 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
574 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
579 r = 0
575 r = 0
580 for p in payload:
576 for p in payload:
581 fp.write(p)
577 fp.write(p)
582 fp.seek(0)
578 fp.seek(0)
583
579
584 gen = exchange.readbundle(repo.ui, fp, None)
580 gen = exchange.readbundle(repo.ui, fp, None)
585 if (isinstance(gen, changegroupmod.cg1unpacker)
581 if (isinstance(gen, changegroupmod.cg1unpacker)
586 and not bundle1allowed(repo, 'push')):
582 and not bundle1allowed(repo, 'push')):
587 if proto.name == 'http-v1':
583 if proto.name == 'http-v1':
588 # need to special case http because stderr do not get to
584 # need to special case http because stderr do not get to
589 # the http client on failed push so we need to abuse
585 # the http client on failed push so we need to abuse
590 # some other error type to make sure the message get to
586 # some other error type to make sure the message get to
591 # the user.
587 # the user.
592 return wireprototypes.ooberror(bundle2required)
588 return wireprototypes.ooberror(bundle2required)
593 raise error.Abort(bundle2requiredmain,
589 raise error.Abort(bundle2requiredmain,
594 hint=bundle2requiredhint)
590 hint=bundle2requiredhint)
595
591
596 r = exchange.unbundle(repo, gen, their_heads, 'serve',
592 r = exchange.unbundle(repo, gen, their_heads, 'serve',
597 proto.client())
593 proto.client())
598 if util.safehasattr(r, 'addpart'):
594 if util.safehasattr(r, 'addpart'):
599 # The return looks streamable, we are in the bundle2 case
595 # The return looks streamable, we are in the bundle2 case
600 # and should return a stream.
596 # and should return a stream.
601 return wireprototypes.streamreslegacy(gen=r.getchunks())
597 return wireprototypes.streamreslegacy(gen=r.getchunks())
602 return wireprototypes.pushres(
598 return wireprototypes.pushres(
603 r, output.getvalue() if output else '')
599 r, output.getvalue() if output else '')
604
600
605 finally:
601 finally:
606 cleanup()
602 cleanup()
607
603
608 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
604 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
609 # handle non-bundle2 case first
605 # handle non-bundle2 case first
610 if not getattr(exc, 'duringunbundle2', False):
606 if not getattr(exc, 'duringunbundle2', False):
611 try:
607 try:
612 raise
608 raise
613 except error.Abort:
609 except error.Abort:
614 # The old code we moved used procutil.stderr directly.
610 # The old code we moved used procutil.stderr directly.
615 # We did not change it to minimise code change.
611 # We did not change it to minimise code change.
616 # This need to be moved to something proper.
612 # This need to be moved to something proper.
617 # Feel free to do it.
613 # Feel free to do it.
618 procutil.stderr.write("abort: %s\n" % exc)
614 procutil.stderr.write("abort: %s\n" % exc)
619 if exc.hint is not None:
615 if exc.hint is not None:
620 procutil.stderr.write("(%s)\n" % exc.hint)
616 procutil.stderr.write("(%s)\n" % exc.hint)
621 procutil.stderr.flush()
617 procutil.stderr.flush()
622 return wireprototypes.pushres(
618 return wireprototypes.pushres(
623 0, output.getvalue() if output else '')
619 0, output.getvalue() if output else '')
624 except error.PushRaced:
620 except error.PushRaced:
625 return wireprototypes.pusherr(
621 return wireprototypes.pusherr(
626 pycompat.bytestr(exc),
622 pycompat.bytestr(exc),
627 output.getvalue() if output else '')
623 output.getvalue() if output else '')
628
624
629 bundler = bundle2.bundle20(repo.ui)
625 bundler = bundle2.bundle20(repo.ui)
630 for out in getattr(exc, '_bundle2salvagedoutput', ()):
626 for out in getattr(exc, '_bundle2salvagedoutput', ()):
631 bundler.addpart(out)
627 bundler.addpart(out)
632 try:
628 try:
633 try:
629 try:
634 raise
630 raise
635 except error.PushkeyFailed as exc:
631 except error.PushkeyFailed as exc:
636 # check client caps
632 # check client caps
637 remotecaps = getattr(exc, '_replycaps', None)
633 remotecaps = getattr(exc, '_replycaps', None)
638 if (remotecaps is not None
634 if (remotecaps is not None
639 and 'pushkey' not in remotecaps.get('error', ())):
635 and 'pushkey' not in remotecaps.get('error', ())):
640 # no support remote side, fallback to Abort handler.
636 # no support remote side, fallback to Abort handler.
641 raise
637 raise
642 part = bundler.newpart('error:pushkey')
638 part = bundler.newpart('error:pushkey')
643 part.addparam('in-reply-to', exc.partid)
639 part.addparam('in-reply-to', exc.partid)
644 if exc.namespace is not None:
640 if exc.namespace is not None:
645 part.addparam('namespace', exc.namespace,
641 part.addparam('namespace', exc.namespace,
646 mandatory=False)
642 mandatory=False)
647 if exc.key is not None:
643 if exc.key is not None:
648 part.addparam('key', exc.key, mandatory=False)
644 part.addparam('key', exc.key, mandatory=False)
649 if exc.new is not None:
645 if exc.new is not None:
650 part.addparam('new', exc.new, mandatory=False)
646 part.addparam('new', exc.new, mandatory=False)
651 if exc.old is not None:
647 if exc.old is not None:
652 part.addparam('old', exc.old, mandatory=False)
648 part.addparam('old', exc.old, mandatory=False)
653 if exc.ret is not None:
649 if exc.ret is not None:
654 part.addparam('ret', exc.ret, mandatory=False)
650 part.addparam('ret', exc.ret, mandatory=False)
655 except error.BundleValueError as exc:
651 except error.BundleValueError as exc:
656 errpart = bundler.newpart('error:unsupportedcontent')
652 errpart = bundler.newpart('error:unsupportedcontent')
657 if exc.parttype is not None:
653 if exc.parttype is not None:
658 errpart.addparam('parttype', exc.parttype)
654 errpart.addparam('parttype', exc.parttype)
659 if exc.params:
655 if exc.params:
660 errpart.addparam('params', '\0'.join(exc.params))
656 errpart.addparam('params', '\0'.join(exc.params))
661 except error.Abort as exc:
657 except error.Abort as exc:
662 manargs = [('message', stringutil.forcebytestr(exc))]
658 manargs = [('message', stringutil.forcebytestr(exc))]
663 advargs = []
659 advargs = []
664 if exc.hint is not None:
660 if exc.hint is not None:
665 advargs.append(('hint', exc.hint))
661 advargs.append(('hint', exc.hint))
666 bundler.addpart(bundle2.bundlepart('error:abort',
662 bundler.addpart(bundle2.bundlepart('error:abort',
667 manargs, advargs))
663 manargs, advargs))
668 except error.PushRaced as exc:
664 except error.PushRaced as exc:
669 bundler.newpart('error:pushraced',
665 bundler.newpart('error:pushraced',
670 [('message', stringutil.forcebytestr(exc))])
666 [('message', stringutil.forcebytestr(exc))])
671 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
667 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
@@ -1,533 +1,534
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10
10
11 from .i18n import _
11 from .i18n import _
12 from .thirdparty import (
12 from .thirdparty import (
13 cbor,
13 cbor,
14 )
14 )
15 from .thirdparty.zope import (
15 from .thirdparty.zope import (
16 interface as zi,
16 interface as zi,
17 )
17 )
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 pycompat,
21 pycompat,
22 streamclone,
22 streamclone,
23 util,
23 util,
24 wireproto,
25 wireprotoframing,
24 wireprotoframing,
26 wireprototypes,
25 wireprototypes,
27 )
26 )
28
27
29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
28 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
30
29
31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
30 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
32
31
32 COMMANDS = wireprototypes.commanddict()
33
33 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
34 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
34 from .hgweb import common as hgwebcommon
35 from .hgweb import common as hgwebcommon
35
36
36 # URL space looks like: <permissions>/<command>, where <permission> can
37 # URL space looks like: <permissions>/<command>, where <permission> can
37 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
38 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
38
39
39 # Root URL does nothing meaningful... yet.
40 # Root URL does nothing meaningful... yet.
40 if not urlparts:
41 if not urlparts:
41 res.status = b'200 OK'
42 res.status = b'200 OK'
42 res.headers[b'Content-Type'] = b'text/plain'
43 res.headers[b'Content-Type'] = b'text/plain'
43 res.setbodybytes(_('HTTP version 2 API handler'))
44 res.setbodybytes(_('HTTP version 2 API handler'))
44 return
45 return
45
46
46 if len(urlparts) == 1:
47 if len(urlparts) == 1:
47 res.status = b'404 Not Found'
48 res.status = b'404 Not Found'
48 res.headers[b'Content-Type'] = b'text/plain'
49 res.headers[b'Content-Type'] = b'text/plain'
49 res.setbodybytes(_('do not know how to process %s\n') %
50 res.setbodybytes(_('do not know how to process %s\n') %
50 req.dispatchpath)
51 req.dispatchpath)
51 return
52 return
52
53
53 permission, command = urlparts[0:2]
54 permission, command = urlparts[0:2]
54
55
55 if permission not in (b'ro', b'rw'):
56 if permission not in (b'ro', b'rw'):
56 res.status = b'404 Not Found'
57 res.status = b'404 Not Found'
57 res.headers[b'Content-Type'] = b'text/plain'
58 res.headers[b'Content-Type'] = b'text/plain'
58 res.setbodybytes(_('unknown permission: %s') % permission)
59 res.setbodybytes(_('unknown permission: %s') % permission)
59 return
60 return
60
61
61 if req.method != 'POST':
62 if req.method != 'POST':
62 res.status = b'405 Method Not Allowed'
63 res.status = b'405 Method Not Allowed'
63 res.headers[b'Allow'] = b'POST'
64 res.headers[b'Allow'] = b'POST'
64 res.setbodybytes(_('commands require POST requests'))
65 res.setbodybytes(_('commands require POST requests'))
65 return
66 return
66
67
67 # At some point we'll want to use our own API instead of recycling the
68 # At some point we'll want to use our own API instead of recycling the
68 # behavior of version 1 of the wire protocol...
69 # behavior of version 1 of the wire protocol...
69 # TODO return reasonable responses - not responses that overload the
70 # TODO return reasonable responses - not responses that overload the
70 # HTTP status line message for error reporting.
71 # HTTP status line message for error reporting.
71 try:
72 try:
72 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
73 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
73 except hgwebcommon.ErrorResponse as e:
74 except hgwebcommon.ErrorResponse as e:
74 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
75 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
75 for k, v in e.headers:
76 for k, v in e.headers:
76 res.headers[k] = v
77 res.headers[k] = v
77 res.setbodybytes('permission denied')
78 res.setbodybytes('permission denied')
78 return
79 return
79
80
80 # We have a special endpoint to reflect the request back at the client.
81 # We have a special endpoint to reflect the request back at the client.
81 if command == b'debugreflect':
82 if command == b'debugreflect':
82 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
83 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
83 return
84 return
84
85
85 # Extra commands that we handle that aren't really wire protocol
86 # Extra commands that we handle that aren't really wire protocol
86 # commands. Think extra hard before making this hackery available to
87 # commands. Think extra hard before making this hackery available to
87 # extension.
88 # extension.
88 extracommands = {'multirequest'}
89 extracommands = {'multirequest'}
89
90
90 if command not in wireproto.commandsv2 and command not in extracommands:
91 if command not in COMMANDS and command not in extracommands:
91 res.status = b'404 Not Found'
92 res.status = b'404 Not Found'
92 res.headers[b'Content-Type'] = b'text/plain'
93 res.headers[b'Content-Type'] = b'text/plain'
93 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
94 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
94 return
95 return
95
96
96 repo = rctx.repo
97 repo = rctx.repo
97 ui = repo.ui
98 ui = repo.ui
98
99
99 proto = httpv2protocolhandler(req, ui)
100 proto = httpv2protocolhandler(req, ui)
100
101
101 if (not wireproto.commandsv2.commandavailable(command, proto)
102 if (not COMMANDS.commandavailable(command, proto)
102 and command not in extracommands):
103 and command not in extracommands):
103 res.status = b'404 Not Found'
104 res.status = b'404 Not Found'
104 res.headers[b'Content-Type'] = b'text/plain'
105 res.headers[b'Content-Type'] = b'text/plain'
105 res.setbodybytes(_('invalid wire protocol command: %s') % command)
106 res.setbodybytes(_('invalid wire protocol command: %s') % command)
106 return
107 return
107
108
108 # TODO consider cases where proxies may add additional Accept headers.
109 # TODO consider cases where proxies may add additional Accept headers.
109 if req.headers.get(b'Accept') != FRAMINGTYPE:
110 if req.headers.get(b'Accept') != FRAMINGTYPE:
110 res.status = b'406 Not Acceptable'
111 res.status = b'406 Not Acceptable'
111 res.headers[b'Content-Type'] = b'text/plain'
112 res.headers[b'Content-Type'] = b'text/plain'
112 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
113 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
113 % FRAMINGTYPE)
114 % FRAMINGTYPE)
114 return
115 return
115
116
116 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
117 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
117 res.status = b'415 Unsupported Media Type'
118 res.status = b'415 Unsupported Media Type'
118 # TODO we should send a response with appropriate media type,
119 # TODO we should send a response with appropriate media type,
119 # since client does Accept it.
120 # since client does Accept it.
120 res.headers[b'Content-Type'] = b'text/plain'
121 res.headers[b'Content-Type'] = b'text/plain'
121 res.setbodybytes(_('client MUST send Content-Type header with '
122 res.setbodybytes(_('client MUST send Content-Type header with '
122 'value: %s\n') % FRAMINGTYPE)
123 'value: %s\n') % FRAMINGTYPE)
123 return
124 return
124
125
125 _processhttpv2request(ui, repo, req, res, permission, command, proto)
126 _processhttpv2request(ui, repo, req, res, permission, command, proto)
126
127
127 def _processhttpv2reflectrequest(ui, repo, req, res):
128 def _processhttpv2reflectrequest(ui, repo, req, res):
128 """Reads unified frame protocol request and dumps out state to client.
129 """Reads unified frame protocol request and dumps out state to client.
129
130
130 This special endpoint can be used to help debug the wire protocol.
131 This special endpoint can be used to help debug the wire protocol.
131
132
132 Instead of routing the request through the normal dispatch mechanism,
133 Instead of routing the request through the normal dispatch mechanism,
133 we instead read all frames, decode them, and feed them into our state
134 we instead read all frames, decode them, and feed them into our state
134 tracker. We then dump the log of all that activity back out to the
135 tracker. We then dump the log of all that activity back out to the
135 client.
136 client.
136 """
137 """
137 import json
138 import json
138
139
139 # Reflection APIs have a history of being abused, accidentally disclosing
140 # Reflection APIs have a history of being abused, accidentally disclosing
140 # sensitive data, etc. So we have a config knob.
141 # sensitive data, etc. So we have a config knob.
141 if not ui.configbool('experimental', 'web.api.debugreflect'):
142 if not ui.configbool('experimental', 'web.api.debugreflect'):
142 res.status = b'404 Not Found'
143 res.status = b'404 Not Found'
143 res.headers[b'Content-Type'] = b'text/plain'
144 res.headers[b'Content-Type'] = b'text/plain'
144 res.setbodybytes(_('debugreflect service not available'))
145 res.setbodybytes(_('debugreflect service not available'))
145 return
146 return
146
147
147 # We assume we have a unified framing protocol request body.
148 # We assume we have a unified framing protocol request body.
148
149
149 reactor = wireprotoframing.serverreactor()
150 reactor = wireprotoframing.serverreactor()
150 states = []
151 states = []
151
152
152 while True:
153 while True:
153 frame = wireprotoframing.readframe(req.bodyfh)
154 frame = wireprotoframing.readframe(req.bodyfh)
154
155
155 if not frame:
156 if not frame:
156 states.append(b'received: <no frame>')
157 states.append(b'received: <no frame>')
157 break
158 break
158
159
159 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
160 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
160 frame.requestid,
161 frame.requestid,
161 frame.payload))
162 frame.payload))
162
163
163 action, meta = reactor.onframerecv(frame)
164 action, meta = reactor.onframerecv(frame)
164 states.append(json.dumps((action, meta), sort_keys=True,
165 states.append(json.dumps((action, meta), sort_keys=True,
165 separators=(', ', ': ')))
166 separators=(', ', ': ')))
166
167
167 action, meta = reactor.oninputeof()
168 action, meta = reactor.oninputeof()
168 meta['action'] = action
169 meta['action'] = action
169 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
170 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
170
171
171 res.status = b'200 OK'
172 res.status = b'200 OK'
172 res.headers[b'Content-Type'] = b'text/plain'
173 res.headers[b'Content-Type'] = b'text/plain'
173 res.setbodybytes(b'\n'.join(states))
174 res.setbodybytes(b'\n'.join(states))
174
175
175 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
176 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
176 """Post-validation handler for HTTPv2 requests.
177 """Post-validation handler for HTTPv2 requests.
177
178
178 Called when the HTTP request contains unified frame-based protocol
179 Called when the HTTP request contains unified frame-based protocol
179 frames for evaluation.
180 frames for evaluation.
180 """
181 """
181 # TODO Some HTTP clients are full duplex and can receive data before
182 # TODO Some HTTP clients are full duplex and can receive data before
182 # the entire request is transmitted. Figure out a way to indicate support
183 # the entire request is transmitted. Figure out a way to indicate support
183 # for that so we can opt into full duplex mode.
184 # for that so we can opt into full duplex mode.
184 reactor = wireprotoframing.serverreactor(deferoutput=True)
185 reactor = wireprotoframing.serverreactor(deferoutput=True)
185 seencommand = False
186 seencommand = False
186
187
187 outstream = reactor.makeoutputstream()
188 outstream = reactor.makeoutputstream()
188
189
189 while True:
190 while True:
190 frame = wireprotoframing.readframe(req.bodyfh)
191 frame = wireprotoframing.readframe(req.bodyfh)
191 if not frame:
192 if not frame:
192 break
193 break
193
194
194 action, meta = reactor.onframerecv(frame)
195 action, meta = reactor.onframerecv(frame)
195
196
196 if action == 'wantframe':
197 if action == 'wantframe':
197 # Need more data before we can do anything.
198 # Need more data before we can do anything.
198 continue
199 continue
199 elif action == 'runcommand':
200 elif action == 'runcommand':
200 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
201 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
201 reqcommand, reactor, outstream,
202 reqcommand, reactor, outstream,
202 meta, issubsequent=seencommand)
203 meta, issubsequent=seencommand)
203
204
204 if sentoutput:
205 if sentoutput:
205 return
206 return
206
207
207 seencommand = True
208 seencommand = True
208
209
209 elif action == 'error':
210 elif action == 'error':
210 # TODO define proper error mechanism.
211 # TODO define proper error mechanism.
211 res.status = b'200 OK'
212 res.status = b'200 OK'
212 res.headers[b'Content-Type'] = b'text/plain'
213 res.headers[b'Content-Type'] = b'text/plain'
213 res.setbodybytes(meta['message'] + b'\n')
214 res.setbodybytes(meta['message'] + b'\n')
214 return
215 return
215 else:
216 else:
216 raise error.ProgrammingError(
217 raise error.ProgrammingError(
217 'unhandled action from frame processor: %s' % action)
218 'unhandled action from frame processor: %s' % action)
218
219
219 action, meta = reactor.oninputeof()
220 action, meta = reactor.oninputeof()
220 if action == 'sendframes':
221 if action == 'sendframes':
221 # We assume we haven't started sending the response yet. If we're
222 # We assume we haven't started sending the response yet. If we're
222 # wrong, the response type will raise an exception.
223 # wrong, the response type will raise an exception.
223 res.status = b'200 OK'
224 res.status = b'200 OK'
224 res.headers[b'Content-Type'] = FRAMINGTYPE
225 res.headers[b'Content-Type'] = FRAMINGTYPE
225 res.setbodygen(meta['framegen'])
226 res.setbodygen(meta['framegen'])
226 elif action == 'noop':
227 elif action == 'noop':
227 pass
228 pass
228 else:
229 else:
229 raise error.ProgrammingError('unhandled action from frame processor: %s'
230 raise error.ProgrammingError('unhandled action from frame processor: %s'
230 % action)
231 % action)
231
232
232 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
233 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
233 outstream, command, issubsequent):
234 outstream, command, issubsequent):
234 """Dispatch a wire protocol command made from HTTPv2 requests.
235 """Dispatch a wire protocol command made from HTTPv2 requests.
235
236
236 The authenticated permission (``authedperm``) along with the original
237 The authenticated permission (``authedperm``) along with the original
237 command from the URL (``reqcommand``) are passed in.
238 command from the URL (``reqcommand``) are passed in.
238 """
239 """
239 # We already validated that the session has permissions to perform the
240 # We already validated that the session has permissions to perform the
240 # actions in ``authedperm``. In the unified frame protocol, the canonical
241 # actions in ``authedperm``. In the unified frame protocol, the canonical
241 # command to run is expressed in a frame. However, the URL also requested
242 # command to run is expressed in a frame. However, the URL also requested
242 # to run a specific command. We need to be careful that the command we
243 # to run a specific command. We need to be careful that the command we
243 # run doesn't have permissions requirements greater than what was granted
244 # run doesn't have permissions requirements greater than what was granted
244 # by ``authedperm``.
245 # by ``authedperm``.
245 #
246 #
246 # Our rule for this is we only allow one command per HTTP request and
247 # Our rule for this is we only allow one command per HTTP request and
247 # that command must match the command in the URL. However, we make
248 # that command must match the command in the URL. However, we make
248 # an exception for the ``multirequest`` URL. This URL is allowed to
249 # an exception for the ``multirequest`` URL. This URL is allowed to
249 # execute multiple commands. We double check permissions of each command
250 # execute multiple commands. We double check permissions of each command
250 # as it is invoked to ensure there is no privilege escalation.
251 # as it is invoked to ensure there is no privilege escalation.
251 # TODO consider allowing multiple commands to regular command URLs
252 # TODO consider allowing multiple commands to regular command URLs
252 # iff each command is the same.
253 # iff each command is the same.
253
254
254 proto = httpv2protocolhandler(req, ui, args=command['args'])
255 proto = httpv2protocolhandler(req, ui, args=command['args'])
255
256
256 if reqcommand == b'multirequest':
257 if reqcommand == b'multirequest':
257 if not wireproto.commandsv2.commandavailable(command['command'], proto):
258 if not COMMANDS.commandavailable(command['command'], proto):
258 # TODO proper error mechanism
259 # TODO proper error mechanism
259 res.status = b'200 OK'
260 res.status = b'200 OK'
260 res.headers[b'Content-Type'] = b'text/plain'
261 res.headers[b'Content-Type'] = b'text/plain'
261 res.setbodybytes(_('wire protocol command not available: %s') %
262 res.setbodybytes(_('wire protocol command not available: %s') %
262 command['command'])
263 command['command'])
263 return True
264 return True
264
265
265 # TODO don't use assert here, since it may be elided by -O.
266 # TODO don't use assert here, since it may be elided by -O.
266 assert authedperm in (b'ro', b'rw')
267 assert authedperm in (b'ro', b'rw')
267 wirecommand = wireproto.commandsv2[command['command']]
268 wirecommand = COMMANDS[command['command']]
268 assert wirecommand.permission in ('push', 'pull')
269 assert wirecommand.permission in ('push', 'pull')
269
270
270 if authedperm == b'ro' and wirecommand.permission != 'pull':
271 if authedperm == b'ro' and wirecommand.permission != 'pull':
271 # TODO proper error mechanism
272 # TODO proper error mechanism
272 res.status = b'403 Forbidden'
273 res.status = b'403 Forbidden'
273 res.headers[b'Content-Type'] = b'text/plain'
274 res.headers[b'Content-Type'] = b'text/plain'
274 res.setbodybytes(_('insufficient permissions to execute '
275 res.setbodybytes(_('insufficient permissions to execute '
275 'command: %s') % command['command'])
276 'command: %s') % command['command'])
276 return True
277 return True
277
278
278 # TODO should we also call checkperm() here? Maybe not if we're going
279 # TODO should we also call checkperm() here? Maybe not if we're going
279 # to overhaul that API. The granted scope from the URL check should
280 # to overhaul that API. The granted scope from the URL check should
280 # be good enough.
281 # be good enough.
281
282
282 else:
283 else:
283 # Don't allow multiple commands outside of ``multirequest`` URL.
284 # Don't allow multiple commands outside of ``multirequest`` URL.
284 if issubsequent:
285 if issubsequent:
285 # TODO proper error mechanism
286 # TODO proper error mechanism
286 res.status = b'200 OK'
287 res.status = b'200 OK'
287 res.headers[b'Content-Type'] = b'text/plain'
288 res.headers[b'Content-Type'] = b'text/plain'
288 res.setbodybytes(_('multiple commands cannot be issued to this '
289 res.setbodybytes(_('multiple commands cannot be issued to this '
289 'URL'))
290 'URL'))
290 return True
291 return True
291
292
292 if reqcommand != command['command']:
293 if reqcommand != command['command']:
293 # TODO define proper error mechanism
294 # TODO define proper error mechanism
294 res.status = b'200 OK'
295 res.status = b'200 OK'
295 res.headers[b'Content-Type'] = b'text/plain'
296 res.headers[b'Content-Type'] = b'text/plain'
296 res.setbodybytes(_('command in frame must match command in URL'))
297 res.setbodybytes(_('command in frame must match command in URL'))
297 return True
298 return True
298
299
299 rsp = dispatch(repo, proto, command['command'])
300 rsp = dispatch(repo, proto, command['command'])
300
301
301 res.status = b'200 OK'
302 res.status = b'200 OK'
302 res.headers[b'Content-Type'] = FRAMINGTYPE
303 res.headers[b'Content-Type'] = FRAMINGTYPE
303
304
304 if isinstance(rsp, wireprototypes.cborresponse):
305 if isinstance(rsp, wireprototypes.cborresponse):
305 encoded = cbor.dumps(rsp.value, canonical=True)
306 encoded = cbor.dumps(rsp.value, canonical=True)
306 action, meta = reactor.oncommandresponseready(outstream,
307 action, meta = reactor.oncommandresponseready(outstream,
307 command['requestid'],
308 command['requestid'],
308 encoded)
309 encoded)
309 elif isinstance(rsp, wireprototypes.v2streamingresponse):
310 elif isinstance(rsp, wireprototypes.v2streamingresponse):
310 action, meta = reactor.oncommandresponsereadygen(outstream,
311 action, meta = reactor.oncommandresponsereadygen(outstream,
311 command['requestid'],
312 command['requestid'],
312 rsp.gen)
313 rsp.gen)
313 elif isinstance(rsp, wireprototypes.v2errorresponse):
314 elif isinstance(rsp, wireprototypes.v2errorresponse):
314 action, meta = reactor.oncommanderror(outstream,
315 action, meta = reactor.oncommanderror(outstream,
315 command['requestid'],
316 command['requestid'],
316 rsp.message,
317 rsp.message,
317 rsp.args)
318 rsp.args)
318 else:
319 else:
319 action, meta = reactor.onservererror(
320 action, meta = reactor.onservererror(
320 _('unhandled response type from wire proto command'))
321 _('unhandled response type from wire proto command'))
321
322
322 if action == 'sendframes':
323 if action == 'sendframes':
323 res.setbodygen(meta['framegen'])
324 res.setbodygen(meta['framegen'])
324 return True
325 return True
325 elif action == 'noop':
326 elif action == 'noop':
326 return False
327 return False
327 else:
328 else:
328 raise error.ProgrammingError('unhandled event from reactor: %s' %
329 raise error.ProgrammingError('unhandled event from reactor: %s' %
329 action)
330 action)
330
331
331 def getdispatchrepo(repo, proto, command):
332 def getdispatchrepo(repo, proto, command):
332 return repo.filtered('served')
333 return repo.filtered('served')
333
334
334 def dispatch(repo, proto, command):
335 def dispatch(repo, proto, command):
335 repo = getdispatchrepo(repo, proto, command)
336 repo = getdispatchrepo(repo, proto, command)
336
337
337 func, spec = wireproto.commandsv2[command]
338 func, spec = COMMANDS[command]
338 args = proto.getargs(spec)
339 args = proto.getargs(spec)
339
340
340 return func(repo, proto, **args)
341 return func(repo, proto, **args)
341
342
342 @zi.implementer(wireprototypes.baseprotocolhandler)
343 @zi.implementer(wireprototypes.baseprotocolhandler)
343 class httpv2protocolhandler(object):
344 class httpv2protocolhandler(object):
344 def __init__(self, req, ui, args=None):
345 def __init__(self, req, ui, args=None):
345 self._req = req
346 self._req = req
346 self._ui = ui
347 self._ui = ui
347 self._args = args
348 self._args = args
348
349
349 @property
350 @property
350 def name(self):
351 def name(self):
351 return HTTP_WIREPROTO_V2
352 return HTTP_WIREPROTO_V2
352
353
353 def getargs(self, args):
354 def getargs(self, args):
354 data = {}
355 data = {}
355 for k, typ in args.items():
356 for k, typ in args.items():
356 if k == '*':
357 if k == '*':
357 raise NotImplementedError('do not support * args')
358 raise NotImplementedError('do not support * args')
358 elif k in self._args:
359 elif k in self._args:
359 # TODO consider validating value types.
360 # TODO consider validating value types.
360 data[k] = self._args[k]
361 data[k] = self._args[k]
361
362
362 return data
363 return data
363
364
364 def getprotocaps(self):
365 def getprotocaps(self):
365 # Protocol capabilities are currently not implemented for HTTP V2.
366 # Protocol capabilities are currently not implemented for HTTP V2.
366 return set()
367 return set()
367
368
368 def getpayload(self):
369 def getpayload(self):
369 raise NotImplementedError
370 raise NotImplementedError
370
371
371 @contextlib.contextmanager
372 @contextlib.contextmanager
372 def mayberedirectstdio(self):
373 def mayberedirectstdio(self):
373 raise NotImplementedError
374 raise NotImplementedError
374
375
375 def client(self):
376 def client(self):
376 raise NotImplementedError
377 raise NotImplementedError
377
378
378 def addcapabilities(self, repo, caps):
379 def addcapabilities(self, repo, caps):
379 return caps
380 return caps
380
381
381 def checkperm(self, perm):
382 def checkperm(self, perm):
382 raise NotImplementedError
383 raise NotImplementedError
383
384
384 def httpv2apidescriptor(req, repo):
385 def httpv2apidescriptor(req, repo):
385 proto = httpv2protocolhandler(req, repo.ui)
386 proto = httpv2protocolhandler(req, repo.ui)
386
387
387 return _capabilitiesv2(repo, proto)
388 return _capabilitiesv2(repo, proto)
388
389
389 def _capabilitiesv2(repo, proto):
390 def _capabilitiesv2(repo, proto):
390 """Obtain the set of capabilities for version 2 transports.
391 """Obtain the set of capabilities for version 2 transports.
391
392
392 These capabilities are distinct from the capabilities for version 1
393 These capabilities are distinct from the capabilities for version 1
393 transports.
394 transports.
394 """
395 """
395 compression = []
396 compression = []
396 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
397 for engine in wireprototypes.supportedcompengines(repo.ui, util.SERVERROLE):
397 compression.append({
398 compression.append({
398 b'name': engine.wireprotosupport().name,
399 b'name': engine.wireprotosupport().name,
399 })
400 })
400
401
401 caps = {
402 caps = {
402 'commands': {},
403 'commands': {},
403 'compression': compression,
404 'compression': compression,
404 'framingmediatypes': [FRAMINGTYPE],
405 'framingmediatypes': [FRAMINGTYPE],
405 }
406 }
406
407
407 for command, entry in wireproto.commandsv2.items():
408 for command, entry in COMMANDS.items():
408 caps['commands'][command] = {
409 caps['commands'][command] = {
409 'args': entry.args,
410 'args': entry.args,
410 'permissions': [entry.permission],
411 'permissions': [entry.permission],
411 }
412 }
412
413
413 if streamclone.allowservergeneration(repo):
414 if streamclone.allowservergeneration(repo):
414 caps['rawrepoformats'] = sorted(repo.requirements &
415 caps['rawrepoformats'] = sorted(repo.requirements &
415 repo.supportedformats)
416 repo.supportedformats)
416
417
417 return proto.addcapabilities(repo, caps)
418 return proto.addcapabilities(repo, caps)
418
419
419 def wireprotocommand(name, args=None, permission='push'):
420 def wireprotocommand(name, args=None, permission='push'):
420 """Decorator to declare a wire protocol command.
421 """Decorator to declare a wire protocol command.
421
422
422 ``name`` is the name of the wire protocol command being provided.
423 ``name`` is the name of the wire protocol command being provided.
423
424
424 ``args`` is a dict of argument names to example values.
425 ``args`` is a dict of argument names to example values.
425
426
426 ``permission`` defines the permission type needed to run this command.
427 ``permission`` defines the permission type needed to run this command.
427 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
428 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
428 respectively. Default is to assume command requires ``push`` permissions
429 respectively. Default is to assume command requires ``push`` permissions
429 because otherwise commands not declaring their permissions could modify
430 because otherwise commands not declaring their permissions could modify
430 a repository that is supposed to be read-only.
431 a repository that is supposed to be read-only.
431 """
432 """
432 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
433 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
433 if v['version'] == 2}
434 if v['version'] == 2}
434
435
435 if permission not in ('push', 'pull'):
436 if permission not in ('push', 'pull'):
436 raise error.ProgrammingError('invalid wire protocol permission; '
437 raise error.ProgrammingError('invalid wire protocol permission; '
437 'got %s; expected "push" or "pull"' %
438 'got %s; expected "push" or "pull"' %
438 permission)
439 permission)
439
440
440 if args is None:
441 if args is None:
441 args = {}
442 args = {}
442
443
443 if not isinstance(args, dict):
444 if not isinstance(args, dict):
444 raise error.ProgrammingError('arguments for version 2 commands '
445 raise error.ProgrammingError('arguments for version 2 commands '
445 'must be declared as dicts')
446 'must be declared as dicts')
446
447
447 def register(func):
448 def register(func):
448 if name in wireproto.commandsv2:
449 if name in COMMANDS:
449 raise error.ProgrammingError('%s command already registered '
450 raise error.ProgrammingError('%s command already registered '
450 'for version 2' % name)
451 'for version 2' % name)
451
452
452 wireproto.commandsv2[name] = wireprototypes.commandentry(
453 COMMANDS[name] = wireprototypes.commandentry(
453 func, args=args, transports=transports, permission=permission)
454 func, args=args, transports=transports, permission=permission)
454
455
455 return func
456 return func
456
457
457 return register
458 return register
458
459
459 @wireprotocommand('branchmap', permission='pull')
460 @wireprotocommand('branchmap', permission='pull')
460 def branchmapv2(repo, proto):
461 def branchmapv2(repo, proto):
461 branchmap = {encoding.fromlocal(k): v
462 branchmap = {encoding.fromlocal(k): v
462 for k, v in repo.branchmap().iteritems()}
463 for k, v in repo.branchmap().iteritems()}
463
464
464 return wireprototypes.cborresponse(branchmap)
465 return wireprototypes.cborresponse(branchmap)
465
466
466 @wireprotocommand('capabilities', permission='pull')
467 @wireprotocommand('capabilities', permission='pull')
467 def capabilitiesv2(repo, proto):
468 def capabilitiesv2(repo, proto):
468 caps = _capabilitiesv2(repo, proto)
469 caps = _capabilitiesv2(repo, proto)
469
470
470 return wireprototypes.cborresponse(caps)
471 return wireprototypes.cborresponse(caps)
471
472
472 @wireprotocommand('heads',
473 @wireprotocommand('heads',
473 args={
474 args={
474 'publiconly': False,
475 'publiconly': False,
475 },
476 },
476 permission='pull')
477 permission='pull')
477 def headsv2(repo, proto, publiconly=False):
478 def headsv2(repo, proto, publiconly=False):
478 if publiconly:
479 if publiconly:
479 repo = repo.filtered('immutable')
480 repo = repo.filtered('immutable')
480
481
481 return wireprototypes.cborresponse(repo.heads())
482 return wireprototypes.cborresponse(repo.heads())
482
483
483 @wireprotocommand('known',
484 @wireprotocommand('known',
484 args={
485 args={
485 'nodes': [b'deadbeef'],
486 'nodes': [b'deadbeef'],
486 },
487 },
487 permission='pull')
488 permission='pull')
488 def knownv2(repo, proto, nodes=None):
489 def knownv2(repo, proto, nodes=None):
489 nodes = nodes or []
490 nodes = nodes or []
490 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
491 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
491 return wireprototypes.cborresponse(result)
492 return wireprototypes.cborresponse(result)
492
493
493 @wireprotocommand('listkeys',
494 @wireprotocommand('listkeys',
494 args={
495 args={
495 'namespace': b'ns',
496 'namespace': b'ns',
496 },
497 },
497 permission='pull')
498 permission='pull')
498 def listkeysv2(repo, proto, namespace=None):
499 def listkeysv2(repo, proto, namespace=None):
499 keys = repo.listkeys(encoding.tolocal(namespace))
500 keys = repo.listkeys(encoding.tolocal(namespace))
500 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
501 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
501 for k, v in keys.iteritems()}
502 for k, v in keys.iteritems()}
502
503
503 return wireprototypes.cborresponse(keys)
504 return wireprototypes.cborresponse(keys)
504
505
505 @wireprotocommand('lookup',
506 @wireprotocommand('lookup',
506 args={
507 args={
507 'key': b'foo',
508 'key': b'foo',
508 },
509 },
509 permission='pull')
510 permission='pull')
510 def lookupv2(repo, proto, key):
511 def lookupv2(repo, proto, key):
511 key = encoding.tolocal(key)
512 key = encoding.tolocal(key)
512
513
513 # TODO handle exception.
514 # TODO handle exception.
514 node = repo.lookup(key)
515 node = repo.lookup(key)
515
516
516 return wireprototypes.cborresponse(node)
517 return wireprototypes.cborresponse(node)
517
518
518 @wireprotocommand('pushkey',
519 @wireprotocommand('pushkey',
519 args={
520 args={
520 'namespace': b'ns',
521 'namespace': b'ns',
521 'key': b'key',
522 'key': b'key',
522 'old': b'old',
523 'old': b'old',
523 'new': b'new',
524 'new': b'new',
524 },
525 },
525 permission='push')
526 permission='push')
526 def pushkeyv2(repo, proto, namespace, key, old, new):
527 def pushkeyv2(repo, proto, namespace, key, old, new):
527 # TODO handle ui output redirection
528 # TODO handle ui output redirection
528 r = repo.pushkey(encoding.tolocal(namespace),
529 r = repo.pushkey(encoding.tolocal(namespace),
529 encoding.tolocal(key),
530 encoding.tolocal(key),
530 encoding.tolocal(old),
531 encoding.tolocal(old),
531 encoding.tolocal(new))
532 encoding.tolocal(new))
532
533
533 return wireprototypes.cborresponse(r)
534 return wireprototypes.cborresponse(r)
General Comments 0
You need to be logged in to leave comments. Login now