##// END OF EJS Templates
wireproto: only expose "getbundle" and "unbundle" to v1 transports...
Gregory Szorc -
r37557:4a0d58d6 default
parent child Browse files
Show More
@@ -1,1412 +1,1414 b''
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 hashlib
10 import hashlib
11 import os
11 import os
12 import tempfile
12 import tempfile
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 bin,
16 bin,
17 hex,
17 hex,
18 nullid,
18 nullid,
19 )
19 )
20
20
21 from . import (
21 from . import (
22 bundle2,
22 bundle2,
23 changegroup as changegroupmod,
23 changegroup as changegroupmod,
24 discovery,
24 discovery,
25 encoding,
25 encoding,
26 error,
26 error,
27 exchange,
27 exchange,
28 peer,
28 peer,
29 pushkey as pushkeymod,
29 pushkey as pushkeymod,
30 pycompat,
30 pycompat,
31 repository,
31 repository,
32 streamclone,
32 streamclone,
33 util,
33 util,
34 wireprototypes,
34 wireprototypes,
35 )
35 )
36
36
37 from .utils import (
37 from .utils import (
38 procutil,
38 procutil,
39 stringutil,
39 stringutil,
40 )
40 )
41
41
42 urlerr = util.urlerr
42 urlerr = util.urlerr
43 urlreq = util.urlreq
43 urlreq = util.urlreq
44
44
45 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
45 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
46 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
46 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
47 'IncompatibleClient')
47 'IncompatibleClient')
48 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
48 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
49
49
50 class remoteiterbatcher(peer.iterbatcher):
50 class remoteiterbatcher(peer.iterbatcher):
51 def __init__(self, remote):
51 def __init__(self, remote):
52 super(remoteiterbatcher, self).__init__()
52 super(remoteiterbatcher, self).__init__()
53 self._remote = remote
53 self._remote = remote
54
54
55 def __getattr__(self, name):
55 def __getattr__(self, name):
56 # Validate this method is batchable, since submit() only supports
56 # Validate this method is batchable, since submit() only supports
57 # batchable methods.
57 # batchable methods.
58 fn = getattr(self._remote, name)
58 fn = getattr(self._remote, name)
59 if not getattr(fn, 'batchable', None):
59 if not getattr(fn, 'batchable', None):
60 raise error.ProgrammingError('Attempted to batch a non-batchable '
60 raise error.ProgrammingError('Attempted to batch a non-batchable '
61 'call to %r' % name)
61 'call to %r' % name)
62
62
63 return super(remoteiterbatcher, self).__getattr__(name)
63 return super(remoteiterbatcher, self).__getattr__(name)
64
64
65 def submit(self):
65 def submit(self):
66 """Break the batch request into many patch calls and pipeline them.
66 """Break the batch request into many patch calls and pipeline them.
67
67
68 This is mostly valuable over http where request sizes can be
68 This is mostly valuable over http where request sizes can be
69 limited, but can be used in other places as well.
69 limited, but can be used in other places as well.
70 """
70 """
71 # 2-tuple of (command, arguments) that represents what will be
71 # 2-tuple of (command, arguments) that represents what will be
72 # sent over the wire.
72 # sent over the wire.
73 requests = []
73 requests = []
74
74
75 # 4-tuple of (command, final future, @batchable generator, remote
75 # 4-tuple of (command, final future, @batchable generator, remote
76 # future).
76 # future).
77 results = []
77 results = []
78
78
79 for command, args, opts, finalfuture in self.calls:
79 for command, args, opts, finalfuture in self.calls:
80 mtd = getattr(self._remote, command)
80 mtd = getattr(self._remote, command)
81 batchable = mtd.batchable(mtd.__self__, *args, **opts)
81 batchable = mtd.batchable(mtd.__self__, *args, **opts)
82
82
83 commandargs, fremote = next(batchable)
83 commandargs, fremote = next(batchable)
84 assert fremote
84 assert fremote
85 requests.append((command, commandargs))
85 requests.append((command, commandargs))
86 results.append((command, finalfuture, batchable, fremote))
86 results.append((command, finalfuture, batchable, fremote))
87
87
88 if requests:
88 if requests:
89 self._resultiter = self._remote._submitbatch(requests)
89 self._resultiter = self._remote._submitbatch(requests)
90
90
91 self._results = results
91 self._results = results
92
92
93 def results(self):
93 def results(self):
94 for command, finalfuture, batchable, remotefuture in self._results:
94 for command, finalfuture, batchable, remotefuture in self._results:
95 # Get the raw result, set it in the remote future, feed it
95 # Get the raw result, set it in the remote future, feed it
96 # back into the @batchable generator so it can be decoded, and
96 # back into the @batchable generator so it can be decoded, and
97 # set the result on the final future to this value.
97 # set the result on the final future to this value.
98 remoteresult = next(self._resultiter)
98 remoteresult = next(self._resultiter)
99 remotefuture.set(remoteresult)
99 remotefuture.set(remoteresult)
100 finalfuture.set(next(batchable))
100 finalfuture.set(next(batchable))
101
101
102 # Verify our @batchable generators only emit 2 values.
102 # Verify our @batchable generators only emit 2 values.
103 try:
103 try:
104 next(batchable)
104 next(batchable)
105 except StopIteration:
105 except StopIteration:
106 pass
106 pass
107 else:
107 else:
108 raise error.ProgrammingError('%s @batchable generator emitted '
108 raise error.ProgrammingError('%s @batchable generator emitted '
109 'unexpected value count' % command)
109 'unexpected value count' % command)
110
110
111 yield finalfuture.value
111 yield finalfuture.value
112
112
113 # Forward a couple of names from peer to make wireproto interactions
113 # Forward a couple of names from peer to make wireproto interactions
114 # slightly more sensible.
114 # slightly more sensible.
115 batchable = peer.batchable
115 batchable = peer.batchable
116 future = peer.future
116 future = peer.future
117
117
118 # list of nodes encoding / decoding
118 # list of nodes encoding / decoding
119
119
120 def decodelist(l, sep=' '):
120 def decodelist(l, sep=' '):
121 if l:
121 if l:
122 return [bin(v) for v in l.split(sep)]
122 return [bin(v) for v in l.split(sep)]
123 return []
123 return []
124
124
125 def encodelist(l, sep=' '):
125 def encodelist(l, sep=' '):
126 try:
126 try:
127 return sep.join(map(hex, l))
127 return sep.join(map(hex, l))
128 except TypeError:
128 except TypeError:
129 raise
129 raise
130
130
131 # batched call argument encoding
131 # batched call argument encoding
132
132
133 def escapearg(plain):
133 def escapearg(plain):
134 return (plain
134 return (plain
135 .replace(':', ':c')
135 .replace(':', ':c')
136 .replace(',', ':o')
136 .replace(',', ':o')
137 .replace(';', ':s')
137 .replace(';', ':s')
138 .replace('=', ':e'))
138 .replace('=', ':e'))
139
139
140 def unescapearg(escaped):
140 def unescapearg(escaped):
141 return (escaped
141 return (escaped
142 .replace(':e', '=')
142 .replace(':e', '=')
143 .replace(':s', ';')
143 .replace(':s', ';')
144 .replace(':o', ',')
144 .replace(':o', ',')
145 .replace(':c', ':'))
145 .replace(':c', ':'))
146
146
147 def encodebatchcmds(req):
147 def encodebatchcmds(req):
148 """Return a ``cmds`` argument value for the ``batch`` command."""
148 """Return a ``cmds`` argument value for the ``batch`` command."""
149 cmds = []
149 cmds = []
150 for op, argsdict in req:
150 for op, argsdict in req:
151 # Old servers didn't properly unescape argument names. So prevent
151 # Old servers didn't properly unescape argument names. So prevent
152 # the sending of argument names that may not be decoded properly by
152 # the sending of argument names that may not be decoded properly by
153 # servers.
153 # servers.
154 assert all(escapearg(k) == k for k in argsdict)
154 assert all(escapearg(k) == k for k in argsdict)
155
155
156 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
156 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
157 for k, v in argsdict.iteritems())
157 for k, v in argsdict.iteritems())
158 cmds.append('%s %s' % (op, args))
158 cmds.append('%s %s' % (op, args))
159
159
160 return ';'.join(cmds)
160 return ';'.join(cmds)
161
161
162 def clientcompressionsupport(proto):
162 def clientcompressionsupport(proto):
163 """Returns a list of compression methods supported by the client.
163 """Returns a list of compression methods supported by the client.
164
164
165 Returns a list of the compression methods supported by the client
165 Returns a list of the compression methods supported by the client
166 according to the protocol capabilities. If no such capability has
166 according to the protocol capabilities. If no such capability has
167 been announced, fallback to the default of zlib and uncompressed.
167 been announced, fallback to the default of zlib and uncompressed.
168 """
168 """
169 for cap in proto.getprotocaps():
169 for cap in proto.getprotocaps():
170 if cap.startswith('comp='):
170 if cap.startswith('comp='):
171 return cap[5:].split(',')
171 return cap[5:].split(',')
172 return ['zlib', 'none']
172 return ['zlib', 'none']
173
173
174 # mapping of options accepted by getbundle and their types
174 # mapping of options accepted by getbundle and their types
175 #
175 #
176 # Meant to be extended by extensions. It is extensions responsibility to ensure
176 # Meant to be extended by extensions. It is extensions responsibility to ensure
177 # such options are properly processed in exchange.getbundle.
177 # such options are properly processed in exchange.getbundle.
178 #
178 #
179 # supported types are:
179 # supported types are:
180 #
180 #
181 # :nodes: list of binary nodes
181 # :nodes: list of binary nodes
182 # :csv: list of comma-separated values
182 # :csv: list of comma-separated values
183 # :scsv: list of comma-separated values return as set
183 # :scsv: list of comma-separated values return as set
184 # :plain: string with no transformation needed.
184 # :plain: string with no transformation needed.
185 gboptsmap = {'heads': 'nodes',
185 gboptsmap = {'heads': 'nodes',
186 'bookmarks': 'boolean',
186 'bookmarks': 'boolean',
187 'common': 'nodes',
187 'common': 'nodes',
188 'obsmarkers': 'boolean',
188 'obsmarkers': 'boolean',
189 'phases': 'boolean',
189 'phases': 'boolean',
190 'bundlecaps': 'scsv',
190 'bundlecaps': 'scsv',
191 'listkeys': 'csv',
191 'listkeys': 'csv',
192 'cg': 'boolean',
192 'cg': 'boolean',
193 'cbattempted': 'boolean',
193 'cbattempted': 'boolean',
194 'stream': 'boolean',
194 'stream': 'boolean',
195 }
195 }
196
196
197 # client side
197 # client side
198
198
199 class wirepeer(repository.legacypeer):
199 class wirepeer(repository.legacypeer):
200 """Client-side interface for communicating with a peer repository.
200 """Client-side interface for communicating with a peer repository.
201
201
202 Methods commonly call wire protocol commands of the same name.
202 Methods commonly call wire protocol commands of the same name.
203
203
204 See also httppeer.py and sshpeer.py for protocol-specific
204 See also httppeer.py and sshpeer.py for protocol-specific
205 implementations of this interface.
205 implementations of this interface.
206 """
206 """
207 # Begin of ipeercommands interface.
207 # Begin of ipeercommands interface.
208
208
209 def iterbatch(self):
209 def iterbatch(self):
210 return remoteiterbatcher(self)
210 return remoteiterbatcher(self)
211
211
212 @batchable
212 @batchable
213 def lookup(self, key):
213 def lookup(self, key):
214 self.requirecap('lookup', _('look up remote revision'))
214 self.requirecap('lookup', _('look up remote revision'))
215 f = future()
215 f = future()
216 yield {'key': encoding.fromlocal(key)}, f
216 yield {'key': encoding.fromlocal(key)}, f
217 d = f.value
217 d = f.value
218 success, data = d[:-1].split(" ", 1)
218 success, data = d[:-1].split(" ", 1)
219 if int(success):
219 if int(success):
220 yield bin(data)
220 yield bin(data)
221 else:
221 else:
222 self._abort(error.RepoError(data))
222 self._abort(error.RepoError(data))
223
223
224 @batchable
224 @batchable
225 def heads(self):
225 def heads(self):
226 f = future()
226 f = future()
227 yield {}, f
227 yield {}, f
228 d = f.value
228 d = f.value
229 try:
229 try:
230 yield decodelist(d[:-1])
230 yield decodelist(d[:-1])
231 except ValueError:
231 except ValueError:
232 self._abort(error.ResponseError(_("unexpected response:"), d))
232 self._abort(error.ResponseError(_("unexpected response:"), d))
233
233
234 @batchable
234 @batchable
235 def known(self, nodes):
235 def known(self, nodes):
236 f = future()
236 f = future()
237 yield {'nodes': encodelist(nodes)}, f
237 yield {'nodes': encodelist(nodes)}, f
238 d = f.value
238 d = f.value
239 try:
239 try:
240 yield [bool(int(b)) for b in d]
240 yield [bool(int(b)) for b in d]
241 except ValueError:
241 except ValueError:
242 self._abort(error.ResponseError(_("unexpected response:"), d))
242 self._abort(error.ResponseError(_("unexpected response:"), d))
243
243
244 @batchable
244 @batchable
245 def branchmap(self):
245 def branchmap(self):
246 f = future()
246 f = future()
247 yield {}, f
247 yield {}, f
248 d = f.value
248 d = f.value
249 try:
249 try:
250 branchmap = {}
250 branchmap = {}
251 for branchpart in d.splitlines():
251 for branchpart in d.splitlines():
252 branchname, branchheads = branchpart.split(' ', 1)
252 branchname, branchheads = branchpart.split(' ', 1)
253 branchname = encoding.tolocal(urlreq.unquote(branchname))
253 branchname = encoding.tolocal(urlreq.unquote(branchname))
254 branchheads = decodelist(branchheads)
254 branchheads = decodelist(branchheads)
255 branchmap[branchname] = branchheads
255 branchmap[branchname] = branchheads
256 yield branchmap
256 yield branchmap
257 except TypeError:
257 except TypeError:
258 self._abort(error.ResponseError(_("unexpected response:"), d))
258 self._abort(error.ResponseError(_("unexpected response:"), d))
259
259
260 @batchable
260 @batchable
261 def listkeys(self, namespace):
261 def listkeys(self, namespace):
262 if not self.capable('pushkey'):
262 if not self.capable('pushkey'):
263 yield {}, None
263 yield {}, None
264 f = future()
264 f = future()
265 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
265 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
266 yield {'namespace': encoding.fromlocal(namespace)}, f
266 yield {'namespace': encoding.fromlocal(namespace)}, f
267 d = f.value
267 d = f.value
268 self.ui.debug('received listkey for "%s": %i bytes\n'
268 self.ui.debug('received listkey for "%s": %i bytes\n'
269 % (namespace, len(d)))
269 % (namespace, len(d)))
270 yield pushkeymod.decodekeys(d)
270 yield pushkeymod.decodekeys(d)
271
271
272 @batchable
272 @batchable
273 def pushkey(self, namespace, key, old, new):
273 def pushkey(self, namespace, key, old, new):
274 if not self.capable('pushkey'):
274 if not self.capable('pushkey'):
275 yield False, None
275 yield False, None
276 f = future()
276 f = future()
277 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
277 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
278 yield {'namespace': encoding.fromlocal(namespace),
278 yield {'namespace': encoding.fromlocal(namespace),
279 'key': encoding.fromlocal(key),
279 'key': encoding.fromlocal(key),
280 'old': encoding.fromlocal(old),
280 'old': encoding.fromlocal(old),
281 'new': encoding.fromlocal(new)}, f
281 'new': encoding.fromlocal(new)}, f
282 d = f.value
282 d = f.value
283 d, output = d.split('\n', 1)
283 d, output = d.split('\n', 1)
284 try:
284 try:
285 d = bool(int(d))
285 d = bool(int(d))
286 except ValueError:
286 except ValueError:
287 raise error.ResponseError(
287 raise error.ResponseError(
288 _('push failed (unexpected response):'), d)
288 _('push failed (unexpected response):'), d)
289 for l in output.splitlines(True):
289 for l in output.splitlines(True):
290 self.ui.status(_('remote: '), l)
290 self.ui.status(_('remote: '), l)
291 yield d
291 yield d
292
292
293 def stream_out(self):
293 def stream_out(self):
294 return self._callstream('stream_out')
294 return self._callstream('stream_out')
295
295
296 def getbundle(self, source, **kwargs):
296 def getbundle(self, source, **kwargs):
297 kwargs = pycompat.byteskwargs(kwargs)
297 kwargs = pycompat.byteskwargs(kwargs)
298 self.requirecap('getbundle', _('look up remote changes'))
298 self.requirecap('getbundle', _('look up remote changes'))
299 opts = {}
299 opts = {}
300 bundlecaps = kwargs.get('bundlecaps') or set()
300 bundlecaps = kwargs.get('bundlecaps') or set()
301 for key, value in kwargs.iteritems():
301 for key, value in kwargs.iteritems():
302 if value is None:
302 if value is None:
303 continue
303 continue
304 keytype = gboptsmap.get(key)
304 keytype = gboptsmap.get(key)
305 if keytype is None:
305 if keytype is None:
306 raise error.ProgrammingError(
306 raise error.ProgrammingError(
307 'Unexpectedly None keytype for key %s' % key)
307 'Unexpectedly None keytype for key %s' % key)
308 elif keytype == 'nodes':
308 elif keytype == 'nodes':
309 value = encodelist(value)
309 value = encodelist(value)
310 elif keytype == 'csv':
310 elif keytype == 'csv':
311 value = ','.join(value)
311 value = ','.join(value)
312 elif keytype == 'scsv':
312 elif keytype == 'scsv':
313 value = ','.join(sorted(value))
313 value = ','.join(sorted(value))
314 elif keytype == 'boolean':
314 elif keytype == 'boolean':
315 value = '%i' % bool(value)
315 value = '%i' % bool(value)
316 elif keytype != 'plain':
316 elif keytype != 'plain':
317 raise KeyError('unknown getbundle option type %s'
317 raise KeyError('unknown getbundle option type %s'
318 % keytype)
318 % keytype)
319 opts[key] = value
319 opts[key] = value
320 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
320 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
321 if any((cap.startswith('HG2') for cap in bundlecaps)):
321 if any((cap.startswith('HG2') for cap in bundlecaps)):
322 return bundle2.getunbundler(self.ui, f)
322 return bundle2.getunbundler(self.ui, f)
323 else:
323 else:
324 return changegroupmod.cg1unpacker(f, 'UN')
324 return changegroupmod.cg1unpacker(f, 'UN')
325
325
326 def unbundle(self, cg, heads, url):
326 def unbundle(self, cg, heads, url):
327 '''Send cg (a readable file-like object representing the
327 '''Send cg (a readable file-like object representing the
328 changegroup to push, typically a chunkbuffer object) to the
328 changegroup to push, typically a chunkbuffer object) to the
329 remote server as a bundle.
329 remote server as a bundle.
330
330
331 When pushing a bundle10 stream, return an integer indicating the
331 When pushing a bundle10 stream, return an integer indicating the
332 result of the push (see changegroup.apply()).
332 result of the push (see changegroup.apply()).
333
333
334 When pushing a bundle20 stream, return a bundle20 stream.
334 When pushing a bundle20 stream, return a bundle20 stream.
335
335
336 `url` is the url the client thinks it's pushing to, which is
336 `url` is the url the client thinks it's pushing to, which is
337 visible to hooks.
337 visible to hooks.
338 '''
338 '''
339
339
340 if heads != ['force'] and self.capable('unbundlehash'):
340 if heads != ['force'] and self.capable('unbundlehash'):
341 heads = encodelist(['hashed',
341 heads = encodelist(['hashed',
342 hashlib.sha1(''.join(sorted(heads))).digest()])
342 hashlib.sha1(''.join(sorted(heads))).digest()])
343 else:
343 else:
344 heads = encodelist(heads)
344 heads = encodelist(heads)
345
345
346 if util.safehasattr(cg, 'deltaheader'):
346 if util.safehasattr(cg, 'deltaheader'):
347 # this a bundle10, do the old style call sequence
347 # this a bundle10, do the old style call sequence
348 ret, output = self._callpush("unbundle", cg, heads=heads)
348 ret, output = self._callpush("unbundle", cg, heads=heads)
349 if ret == "":
349 if ret == "":
350 raise error.ResponseError(
350 raise error.ResponseError(
351 _('push failed:'), output)
351 _('push failed:'), output)
352 try:
352 try:
353 ret = int(ret)
353 ret = int(ret)
354 except ValueError:
354 except ValueError:
355 raise error.ResponseError(
355 raise error.ResponseError(
356 _('push failed (unexpected response):'), ret)
356 _('push failed (unexpected response):'), ret)
357
357
358 for l in output.splitlines(True):
358 for l in output.splitlines(True):
359 self.ui.status(_('remote: '), l)
359 self.ui.status(_('remote: '), l)
360 else:
360 else:
361 # bundle2 push. Send a stream, fetch a stream.
361 # bundle2 push. Send a stream, fetch a stream.
362 stream = self._calltwowaystream('unbundle', cg, heads=heads)
362 stream = self._calltwowaystream('unbundle', cg, heads=heads)
363 ret = bundle2.getunbundler(self.ui, stream)
363 ret = bundle2.getunbundler(self.ui, stream)
364 return ret
364 return ret
365
365
366 # End of ipeercommands interface.
366 # End of ipeercommands interface.
367
367
368 # Begin of ipeerlegacycommands interface.
368 # Begin of ipeerlegacycommands interface.
369
369
370 def branches(self, nodes):
370 def branches(self, nodes):
371 n = encodelist(nodes)
371 n = encodelist(nodes)
372 d = self._call("branches", nodes=n)
372 d = self._call("branches", nodes=n)
373 try:
373 try:
374 br = [tuple(decodelist(b)) for b in d.splitlines()]
374 br = [tuple(decodelist(b)) for b in d.splitlines()]
375 return br
375 return br
376 except ValueError:
376 except ValueError:
377 self._abort(error.ResponseError(_("unexpected response:"), d))
377 self._abort(error.ResponseError(_("unexpected response:"), d))
378
378
379 def between(self, pairs):
379 def between(self, pairs):
380 batch = 8 # avoid giant requests
380 batch = 8 # avoid giant requests
381 r = []
381 r = []
382 for i in xrange(0, len(pairs), batch):
382 for i in xrange(0, len(pairs), batch):
383 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
383 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
384 d = self._call("between", pairs=n)
384 d = self._call("between", pairs=n)
385 try:
385 try:
386 r.extend(l and decodelist(l) or [] for l in d.splitlines())
386 r.extend(l and decodelist(l) or [] for l in d.splitlines())
387 except ValueError:
387 except ValueError:
388 self._abort(error.ResponseError(_("unexpected response:"), d))
388 self._abort(error.ResponseError(_("unexpected response:"), d))
389 return r
389 return r
390
390
391 def changegroup(self, nodes, kind):
391 def changegroup(self, nodes, kind):
392 n = encodelist(nodes)
392 n = encodelist(nodes)
393 f = self._callcompressable("changegroup", roots=n)
393 f = self._callcompressable("changegroup", roots=n)
394 return changegroupmod.cg1unpacker(f, 'UN')
394 return changegroupmod.cg1unpacker(f, 'UN')
395
395
396 def changegroupsubset(self, bases, heads, kind):
396 def changegroupsubset(self, bases, heads, kind):
397 self.requirecap('changegroupsubset', _('look up remote changes'))
397 self.requirecap('changegroupsubset', _('look up remote changes'))
398 bases = encodelist(bases)
398 bases = encodelist(bases)
399 heads = encodelist(heads)
399 heads = encodelist(heads)
400 f = self._callcompressable("changegroupsubset",
400 f = self._callcompressable("changegroupsubset",
401 bases=bases, heads=heads)
401 bases=bases, heads=heads)
402 return changegroupmod.cg1unpacker(f, 'UN')
402 return changegroupmod.cg1unpacker(f, 'UN')
403
403
404 # End of ipeerlegacycommands interface.
404 # End of ipeerlegacycommands interface.
405
405
406 def _submitbatch(self, req):
406 def _submitbatch(self, req):
407 """run batch request <req> on the server
407 """run batch request <req> on the server
408
408
409 Returns an iterator of the raw responses from the server.
409 Returns an iterator of the raw responses from the server.
410 """
410 """
411 ui = self.ui
411 ui = self.ui
412 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
412 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
413 ui.debug('devel-peer-request: batched-content\n')
413 ui.debug('devel-peer-request: batched-content\n')
414 for op, args in req:
414 for op, args in req:
415 msg = 'devel-peer-request: - %s (%d arguments)\n'
415 msg = 'devel-peer-request: - %s (%d arguments)\n'
416 ui.debug(msg % (op, len(args)))
416 ui.debug(msg % (op, len(args)))
417
417
418 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
418 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
419 chunk = rsp.read(1024)
419 chunk = rsp.read(1024)
420 work = [chunk]
420 work = [chunk]
421 while chunk:
421 while chunk:
422 while ';' not in chunk and chunk:
422 while ';' not in chunk and chunk:
423 chunk = rsp.read(1024)
423 chunk = rsp.read(1024)
424 work.append(chunk)
424 work.append(chunk)
425 merged = ''.join(work)
425 merged = ''.join(work)
426 while ';' in merged:
426 while ';' in merged:
427 one, merged = merged.split(';', 1)
427 one, merged = merged.split(';', 1)
428 yield unescapearg(one)
428 yield unescapearg(one)
429 chunk = rsp.read(1024)
429 chunk = rsp.read(1024)
430 work = [merged, chunk]
430 work = [merged, chunk]
431 yield unescapearg(''.join(work))
431 yield unescapearg(''.join(work))
432
432
433 def _submitone(self, op, args):
433 def _submitone(self, op, args):
434 return self._call(op, **pycompat.strkwargs(args))
434 return self._call(op, **pycompat.strkwargs(args))
435
435
436 def debugwireargs(self, one, two, three=None, four=None, five=None):
436 def debugwireargs(self, one, two, three=None, four=None, five=None):
437 # don't pass optional arguments left at their default value
437 # don't pass optional arguments left at their default value
438 opts = {}
438 opts = {}
439 if three is not None:
439 if three is not None:
440 opts[r'three'] = three
440 opts[r'three'] = three
441 if four is not None:
441 if four is not None:
442 opts[r'four'] = four
442 opts[r'four'] = four
443 return self._call('debugwireargs', one=one, two=two, **opts)
443 return self._call('debugwireargs', one=one, two=two, **opts)
444
444
445 def _call(self, cmd, **args):
445 def _call(self, cmd, **args):
446 """execute <cmd> on the server
446 """execute <cmd> on the server
447
447
448 The command is expected to return a simple string.
448 The command is expected to return a simple string.
449
449
450 returns the server reply as a string."""
450 returns the server reply as a string."""
451 raise NotImplementedError()
451 raise NotImplementedError()
452
452
453 def _callstream(self, cmd, **args):
453 def _callstream(self, cmd, **args):
454 """execute <cmd> on the server
454 """execute <cmd> on the server
455
455
456 The command is expected to return a stream. Note that if the
456 The command is expected to return a stream. Note that if the
457 command doesn't return a stream, _callstream behaves
457 command doesn't return a stream, _callstream behaves
458 differently for ssh and http peers.
458 differently for ssh and http peers.
459
459
460 returns the server reply as a file like object.
460 returns the server reply as a file like object.
461 """
461 """
462 raise NotImplementedError()
462 raise NotImplementedError()
463
463
464 def _callcompressable(self, cmd, **args):
464 def _callcompressable(self, cmd, **args):
465 """execute <cmd> on the server
465 """execute <cmd> on the server
466
466
467 The command is expected to return a stream.
467 The command is expected to return a stream.
468
468
469 The stream may have been compressed in some implementations. This
469 The stream may have been compressed in some implementations. This
470 function takes care of the decompression. This is the only difference
470 function takes care of the decompression. This is the only difference
471 with _callstream.
471 with _callstream.
472
472
473 returns the server reply as a file like object.
473 returns the server reply as a file like object.
474 """
474 """
475 raise NotImplementedError()
475 raise NotImplementedError()
476
476
477 def _callpush(self, cmd, fp, **args):
477 def _callpush(self, cmd, fp, **args):
478 """execute a <cmd> on server
478 """execute a <cmd> on server
479
479
480 The command is expected to be related to a push. Push has a special
480 The command is expected to be related to a push. Push has a special
481 return method.
481 return method.
482
482
483 returns the server reply as a (ret, output) tuple. ret is either
483 returns the server reply as a (ret, output) tuple. ret is either
484 empty (error) or a stringified int.
484 empty (error) or a stringified int.
485 """
485 """
486 raise NotImplementedError()
486 raise NotImplementedError()
487
487
488 def _calltwowaystream(self, cmd, fp, **args):
488 def _calltwowaystream(self, cmd, fp, **args):
489 """execute <cmd> on server
489 """execute <cmd> on server
490
490
491 The command will send a stream to the server and get a stream in reply.
491 The command will send a stream to the server and get a stream in reply.
492 """
492 """
493 raise NotImplementedError()
493 raise NotImplementedError()
494
494
495 def _abort(self, exception):
495 def _abort(self, exception):
496 """clearly abort the wire protocol connection and raise the exception
496 """clearly abort the wire protocol connection and raise the exception
497 """
497 """
498 raise NotImplementedError()
498 raise NotImplementedError()
499
499
500 # server side
500 # server side
501
501
502 # wire protocol command can either return a string or one of these classes.
502 # wire protocol command can either return a string or one of these classes.
503
503
504 def getdispatchrepo(repo, proto, command):
504 def getdispatchrepo(repo, proto, command):
505 """Obtain the repo used for processing wire protocol commands.
505 """Obtain the repo used for processing wire protocol commands.
506
506
507 The intent of this function is to serve as a monkeypatch point for
507 The intent of this function is to serve as a monkeypatch point for
508 extensions that need commands to operate on different repo views under
508 extensions that need commands to operate on different repo views under
509 specialized circumstances.
509 specialized circumstances.
510 """
510 """
511 return repo.filtered('served')
511 return repo.filtered('served')
512
512
513 def dispatch(repo, proto, command):
513 def dispatch(repo, proto, command):
514 repo = getdispatchrepo(repo, proto, command)
514 repo = getdispatchrepo(repo, proto, command)
515
515
516 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
516 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
517 commandtable = commandsv2 if transportversion == 2 else commands
517 commandtable = commandsv2 if transportversion == 2 else commands
518 func, spec = commandtable[command]
518 func, spec = commandtable[command]
519
519
520 args = proto.getargs(spec)
520 args = proto.getargs(spec)
521
521
522 # Version 1 protocols define arguments as a list. Version 2 uses a dict.
522 # Version 1 protocols define arguments as a list. Version 2 uses a dict.
523 if isinstance(args, list):
523 if isinstance(args, list):
524 return func(repo, proto, *args)
524 return func(repo, proto, *args)
525 elif isinstance(args, dict):
525 elif isinstance(args, dict):
526 return func(repo, proto, **args)
526 return func(repo, proto, **args)
527 else:
527 else:
528 raise error.ProgrammingError('unexpected type returned from '
528 raise error.ProgrammingError('unexpected type returned from '
529 'proto.getargs(): %s' % type(args))
529 'proto.getargs(): %s' % type(args))
530
530
531 def options(cmd, keys, others):
531 def options(cmd, keys, others):
532 opts = {}
532 opts = {}
533 for k in keys:
533 for k in keys:
534 if k in others:
534 if k in others:
535 opts[k] = others[k]
535 opts[k] = others[k]
536 del others[k]
536 del others[k]
537 if others:
537 if others:
538 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
538 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
539 % (cmd, ",".join(others)))
539 % (cmd, ",".join(others)))
540 return opts
540 return opts
541
541
542 def bundle1allowed(repo, action):
542 def bundle1allowed(repo, action):
543 """Whether a bundle1 operation is allowed from the server.
543 """Whether a bundle1 operation is allowed from the server.
544
544
545 Priority is:
545 Priority is:
546
546
547 1. server.bundle1gd.<action> (if generaldelta active)
547 1. server.bundle1gd.<action> (if generaldelta active)
548 2. server.bundle1.<action>
548 2. server.bundle1.<action>
549 3. server.bundle1gd (if generaldelta active)
549 3. server.bundle1gd (if generaldelta active)
550 4. server.bundle1
550 4. server.bundle1
551 """
551 """
552 ui = repo.ui
552 ui = repo.ui
553 gd = 'generaldelta' in repo.requirements
553 gd = 'generaldelta' in repo.requirements
554
554
555 if gd:
555 if gd:
556 v = ui.configbool('server', 'bundle1gd.%s' % action)
556 v = ui.configbool('server', 'bundle1gd.%s' % action)
557 if v is not None:
557 if v is not None:
558 return v
558 return v
559
559
560 v = ui.configbool('server', 'bundle1.%s' % action)
560 v = ui.configbool('server', 'bundle1.%s' % action)
561 if v is not None:
561 if v is not None:
562 return v
562 return v
563
563
564 if gd:
564 if gd:
565 v = ui.configbool('server', 'bundle1gd')
565 v = ui.configbool('server', 'bundle1gd')
566 if v is not None:
566 if v is not None:
567 return v
567 return v
568
568
569 return ui.configbool('server', 'bundle1')
569 return ui.configbool('server', 'bundle1')
570
570
571 def supportedcompengines(ui, role):
571 def supportedcompengines(ui, role):
572 """Obtain the list of supported compression engines for a request."""
572 """Obtain the list of supported compression engines for a request."""
573 assert role in (util.CLIENTROLE, util.SERVERROLE)
573 assert role in (util.CLIENTROLE, util.SERVERROLE)
574
574
575 compengines = util.compengines.supportedwireengines(role)
575 compengines = util.compengines.supportedwireengines(role)
576
576
577 # Allow config to override default list and ordering.
577 # Allow config to override default list and ordering.
578 if role == util.SERVERROLE:
578 if role == util.SERVERROLE:
579 configengines = ui.configlist('server', 'compressionengines')
579 configengines = ui.configlist('server', 'compressionengines')
580 config = 'server.compressionengines'
580 config = 'server.compressionengines'
581 else:
581 else:
582 # This is currently implemented mainly to facilitate testing. In most
582 # This is currently implemented mainly to facilitate testing. In most
583 # cases, the server should be in charge of choosing a compression engine
583 # cases, the server should be in charge of choosing a compression engine
584 # because a server has the most to lose from a sub-optimal choice. (e.g.
584 # because a server has the most to lose from a sub-optimal choice. (e.g.
585 # CPU DoS due to an expensive engine or a network DoS due to poor
585 # CPU DoS due to an expensive engine or a network DoS due to poor
586 # compression ratio).
586 # compression ratio).
587 configengines = ui.configlist('experimental',
587 configengines = ui.configlist('experimental',
588 'clientcompressionengines')
588 'clientcompressionengines')
589 config = 'experimental.clientcompressionengines'
589 config = 'experimental.clientcompressionengines'
590
590
591 # No explicit config. Filter out the ones that aren't supposed to be
591 # No explicit config. Filter out the ones that aren't supposed to be
592 # advertised and return default ordering.
592 # advertised and return default ordering.
593 if not configengines:
593 if not configengines:
594 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
594 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
595 return [e for e in compengines
595 return [e for e in compengines
596 if getattr(e.wireprotosupport(), attr) > 0]
596 if getattr(e.wireprotosupport(), attr) > 0]
597
597
598 # If compression engines are listed in the config, assume there is a good
598 # If compression engines are listed in the config, assume there is a good
599 # reason for it (like server operators wanting to achieve specific
599 # reason for it (like server operators wanting to achieve specific
600 # performance characteristics). So fail fast if the config references
600 # performance characteristics). So fail fast if the config references
601 # unusable compression engines.
601 # unusable compression engines.
602 validnames = set(e.name() for e in compengines)
602 validnames = set(e.name() for e in compengines)
603 invalidnames = set(e for e in configengines if e not in validnames)
603 invalidnames = set(e for e in configengines if e not in validnames)
604 if invalidnames:
604 if invalidnames:
605 raise error.Abort(_('invalid compression engine defined in %s: %s') %
605 raise error.Abort(_('invalid compression engine defined in %s: %s') %
606 (config, ', '.join(sorted(invalidnames))))
606 (config, ', '.join(sorted(invalidnames))))
607
607
608 compengines = [e for e in compengines if e.name() in configengines]
608 compengines = [e for e in compengines if e.name() in configengines]
609 compengines = sorted(compengines,
609 compengines = sorted(compengines,
610 key=lambda e: configengines.index(e.name()))
610 key=lambda e: configengines.index(e.name()))
611
611
612 if not compengines:
612 if not compengines:
613 raise error.Abort(_('%s config option does not specify any known '
613 raise error.Abort(_('%s config option does not specify any known '
614 'compression engines') % config,
614 'compression engines') % config,
615 hint=_('usable compression engines: %s') %
615 hint=_('usable compression engines: %s') %
616 ', '.sorted(validnames))
616 ', '.sorted(validnames))
617
617
618 return compengines
618 return compengines
619
619
620 class commandentry(object):
620 class commandentry(object):
621 """Represents a declared wire protocol command."""
621 """Represents a declared wire protocol command."""
622 def __init__(self, func, args='', transports=None,
622 def __init__(self, func, args='', transports=None,
623 permission='push'):
623 permission='push'):
624 self.func = func
624 self.func = func
625 self.args = args
625 self.args = args
626 self.transports = transports or set()
626 self.transports = transports or set()
627 self.permission = permission
627 self.permission = permission
628
628
629 def _merge(self, func, args):
629 def _merge(self, func, args):
630 """Merge this instance with an incoming 2-tuple.
630 """Merge this instance with an incoming 2-tuple.
631
631
632 This is called when a caller using the old 2-tuple API attempts
632 This is called when a caller using the old 2-tuple API attempts
633 to replace an instance. The incoming values are merged with
633 to replace an instance. The incoming values are merged with
634 data not captured by the 2-tuple and a new instance containing
634 data not captured by the 2-tuple and a new instance containing
635 the union of the two objects is returned.
635 the union of the two objects is returned.
636 """
636 """
637 return commandentry(func, args=args, transports=set(self.transports),
637 return commandentry(func, args=args, transports=set(self.transports),
638 permission=self.permission)
638 permission=self.permission)
639
639
640 # Old code treats instances as 2-tuples. So expose that interface.
640 # Old code treats instances as 2-tuples. So expose that interface.
641 def __iter__(self):
641 def __iter__(self):
642 yield self.func
642 yield self.func
643 yield self.args
643 yield self.args
644
644
645 def __getitem__(self, i):
645 def __getitem__(self, i):
646 if i == 0:
646 if i == 0:
647 return self.func
647 return self.func
648 elif i == 1:
648 elif i == 1:
649 return self.args
649 return self.args
650 else:
650 else:
651 raise IndexError('can only access elements 0 and 1')
651 raise IndexError('can only access elements 0 and 1')
652
652
653 class commanddict(dict):
653 class commanddict(dict):
654 """Container for registered wire protocol commands.
654 """Container for registered wire protocol commands.
655
655
656 It behaves like a dict. But __setitem__ is overwritten to allow silent
656 It behaves like a dict. But __setitem__ is overwritten to allow silent
657 coercion of values from 2-tuples for API compatibility.
657 coercion of values from 2-tuples for API compatibility.
658 """
658 """
659 def __setitem__(self, k, v):
659 def __setitem__(self, k, v):
660 if isinstance(v, commandentry):
660 if isinstance(v, commandentry):
661 pass
661 pass
662 # Cast 2-tuples to commandentry instances.
662 # Cast 2-tuples to commandentry instances.
663 elif isinstance(v, tuple):
663 elif isinstance(v, tuple):
664 if len(v) != 2:
664 if len(v) != 2:
665 raise ValueError('command tuples must have exactly 2 elements')
665 raise ValueError('command tuples must have exactly 2 elements')
666
666
667 # It is common for extensions to wrap wire protocol commands via
667 # It is common for extensions to wrap wire protocol commands via
668 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
668 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
669 # doing this aren't aware of the new API that uses objects to store
669 # doing this aren't aware of the new API that uses objects to store
670 # command entries, we automatically merge old state with new.
670 # command entries, we automatically merge old state with new.
671 if k in self:
671 if k in self:
672 v = self[k]._merge(v[0], v[1])
672 v = self[k]._merge(v[0], v[1])
673 else:
673 else:
674 # Use default values from @wireprotocommand.
674 # Use default values from @wireprotocommand.
675 v = commandentry(v[0], args=v[1],
675 v = commandentry(v[0], args=v[1],
676 transports=set(wireprototypes.TRANSPORTS),
676 transports=set(wireprototypes.TRANSPORTS),
677 permission='push')
677 permission='push')
678 else:
678 else:
679 raise ValueError('command entries must be commandentry instances '
679 raise ValueError('command entries must be commandentry instances '
680 'or 2-tuples')
680 'or 2-tuples')
681
681
682 return super(commanddict, self).__setitem__(k, v)
682 return super(commanddict, self).__setitem__(k, v)
683
683
684 def commandavailable(self, command, proto):
684 def commandavailable(self, command, proto):
685 """Determine if a command is available for the requested protocol."""
685 """Determine if a command is available for the requested protocol."""
686 assert proto.name in wireprototypes.TRANSPORTS
686 assert proto.name in wireprototypes.TRANSPORTS
687
687
688 entry = self.get(command)
688 entry = self.get(command)
689
689
690 if not entry:
690 if not entry:
691 return False
691 return False
692
692
693 if proto.name not in entry.transports:
693 if proto.name not in entry.transports:
694 return False
694 return False
695
695
696 return True
696 return True
697
697
698 # Constants specifying which transports a wire protocol command should be
698 # Constants specifying which transports a wire protocol command should be
699 # available on. For use with @wireprotocommand.
699 # available on. For use with @wireprotocommand.
700 POLICY_ALL = 'all'
700 POLICY_ALL = 'all'
701 POLICY_V1_ONLY = 'v1-only'
701 POLICY_V1_ONLY = 'v1-only'
702 POLICY_V2_ONLY = 'v2-only'
702 POLICY_V2_ONLY = 'v2-only'
703
703
704 # For version 1 transports.
704 # For version 1 transports.
705 commands = commanddict()
705 commands = commanddict()
706
706
707 # For version 2 transports.
707 # For version 2 transports.
708 commandsv2 = commanddict()
708 commandsv2 = commanddict()
709
709
710 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
710 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
711 permission='push'):
711 permission='push'):
712 """Decorator to declare a wire protocol command.
712 """Decorator to declare a wire protocol command.
713
713
714 ``name`` is the name of the wire protocol command being provided.
714 ``name`` is the name of the wire protocol command being provided.
715
715
716 ``args`` defines the named arguments accepted by the command. It is
716 ``args`` defines the named arguments accepted by the command. It is
717 ideally a dict mapping argument names to their types. For backwards
717 ideally a dict mapping argument names to their types. For backwards
718 compatibility, it can be a space-delimited list of argument names. For
718 compatibility, it can be a space-delimited list of argument names. For
719 version 1 transports, ``*`` denotes a special value that says to accept
719 version 1 transports, ``*`` denotes a special value that says to accept
720 all named arguments.
720 all named arguments.
721
721
722 ``transportpolicy`` is a POLICY_* constant denoting which transports
722 ``transportpolicy`` is a POLICY_* constant denoting which transports
723 this wire protocol command should be exposed to. By default, commands
723 this wire protocol command should be exposed to. By default, commands
724 are exposed to all wire protocol transports.
724 are exposed to all wire protocol transports.
725
725
726 ``permission`` defines the permission type needed to run this command.
726 ``permission`` defines the permission type needed to run this command.
727 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
727 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
728 respectively. Default is to assume command requires ``push`` permissions
728 respectively. Default is to assume command requires ``push`` permissions
729 because otherwise commands not declaring their permissions could modify
729 because otherwise commands not declaring their permissions could modify
730 a repository that is supposed to be read-only.
730 a repository that is supposed to be read-only.
731 """
731 """
732 if transportpolicy == POLICY_ALL:
732 if transportpolicy == POLICY_ALL:
733 transports = set(wireprototypes.TRANSPORTS)
733 transports = set(wireprototypes.TRANSPORTS)
734 transportversions = {1, 2}
734 transportversions = {1, 2}
735 elif transportpolicy == POLICY_V1_ONLY:
735 elif transportpolicy == POLICY_V1_ONLY:
736 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
736 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
737 if v['version'] == 1}
737 if v['version'] == 1}
738 transportversions = {1}
738 transportversions = {1}
739 elif transportpolicy == POLICY_V2_ONLY:
739 elif transportpolicy == POLICY_V2_ONLY:
740 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
740 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
741 if v['version'] == 2}
741 if v['version'] == 2}
742 transportversions = {2}
742 transportversions = {2}
743 else:
743 else:
744 raise error.ProgrammingError('invalid transport policy value: %s' %
744 raise error.ProgrammingError('invalid transport policy value: %s' %
745 transportpolicy)
745 transportpolicy)
746
746
747 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
747 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
748 # SSHv2.
748 # SSHv2.
749 # TODO undo this hack when SSH is using the unified frame protocol.
749 # TODO undo this hack when SSH is using the unified frame protocol.
750 if name == b'batch':
750 if name == b'batch':
751 transports.add(wireprototypes.SSHV2)
751 transports.add(wireprototypes.SSHV2)
752
752
753 if permission not in ('push', 'pull'):
753 if permission not in ('push', 'pull'):
754 raise error.ProgrammingError('invalid wire protocol permission; '
754 raise error.ProgrammingError('invalid wire protocol permission; '
755 'got %s; expected "push" or "pull"' %
755 'got %s; expected "push" or "pull"' %
756 permission)
756 permission)
757
757
758 if 1 in transportversions and not isinstance(args, bytes):
758 if 1 in transportversions and not isinstance(args, bytes):
759 raise error.ProgrammingError('arguments for version 1 commands must '
759 raise error.ProgrammingError('arguments for version 1 commands must '
760 'be declared as bytes')
760 'be declared as bytes')
761
761
762 if isinstance(args, bytes):
762 if isinstance(args, bytes):
763 dictargs = {arg: b'legacy' for arg in args.split()}
763 dictargs = {arg: b'legacy' for arg in args.split()}
764 elif isinstance(args, dict):
764 elif isinstance(args, dict):
765 dictargs = args
765 dictargs = args
766 else:
766 else:
767 raise ValueError('args must be bytes or a dict')
767 raise ValueError('args must be bytes or a dict')
768
768
769 def register(func):
769 def register(func):
770 if 1 in transportversions:
770 if 1 in transportversions:
771 if name in commands:
771 if name in commands:
772 raise error.ProgrammingError('%s command already registered '
772 raise error.ProgrammingError('%s command already registered '
773 'for version 1' % name)
773 'for version 1' % name)
774 commands[name] = commandentry(func, args=args,
774 commands[name] = commandentry(func, args=args,
775 transports=transports,
775 transports=transports,
776 permission=permission)
776 permission=permission)
777 if 2 in transportversions:
777 if 2 in transportversions:
778 if name in commandsv2:
778 if name in commandsv2:
779 raise error.ProgrammingError('%s command already registered '
779 raise error.ProgrammingError('%s command already registered '
780 'for version 2' % name)
780 'for version 2' % name)
781
781
782 commandsv2[name] = commandentry(func, args=dictargs,
782 commandsv2[name] = commandentry(func, args=dictargs,
783 transports=transports,
783 transports=transports,
784 permission=permission)
784 permission=permission)
785
785
786 return func
786 return func
787 return register
787 return register
788
788
789 # TODO define a more appropriate permissions type to use for this.
789 # TODO define a more appropriate permissions type to use for this.
790 @wireprotocommand('batch', 'cmds *', permission='pull',
790 @wireprotocommand('batch', 'cmds *', permission='pull',
791 transportpolicy=POLICY_V1_ONLY)
791 transportpolicy=POLICY_V1_ONLY)
792 def batch(repo, proto, cmds, others):
792 def batch(repo, proto, cmds, others):
793 repo = repo.filtered("served")
793 repo = repo.filtered("served")
794 res = []
794 res = []
795 for pair in cmds.split(';'):
795 for pair in cmds.split(';'):
796 op, args = pair.split(' ', 1)
796 op, args = pair.split(' ', 1)
797 vals = {}
797 vals = {}
798 for a in args.split(','):
798 for a in args.split(','):
799 if a:
799 if a:
800 n, v = a.split('=')
800 n, v = a.split('=')
801 vals[unescapearg(n)] = unescapearg(v)
801 vals[unescapearg(n)] = unescapearg(v)
802 func, spec = commands[op]
802 func, spec = commands[op]
803
803
804 # Validate that client has permissions to perform this command.
804 # Validate that client has permissions to perform this command.
805 perm = commands[op].permission
805 perm = commands[op].permission
806 assert perm in ('push', 'pull')
806 assert perm in ('push', 'pull')
807 proto.checkperm(perm)
807 proto.checkperm(perm)
808
808
809 if spec:
809 if spec:
810 keys = spec.split()
810 keys = spec.split()
811 data = {}
811 data = {}
812 for k in keys:
812 for k in keys:
813 if k == '*':
813 if k == '*':
814 star = {}
814 star = {}
815 for key in vals.keys():
815 for key in vals.keys():
816 if key not in keys:
816 if key not in keys:
817 star[key] = vals[key]
817 star[key] = vals[key]
818 data['*'] = star
818 data['*'] = star
819 else:
819 else:
820 data[k] = vals[k]
820 data[k] = vals[k]
821 result = func(repo, proto, *[data[k] for k in keys])
821 result = func(repo, proto, *[data[k] for k in keys])
822 else:
822 else:
823 result = func(repo, proto)
823 result = func(repo, proto)
824 if isinstance(result, wireprototypes.ooberror):
824 if isinstance(result, wireprototypes.ooberror):
825 return result
825 return result
826
826
827 # For now, all batchable commands must return bytesresponse or
827 # For now, all batchable commands must return bytesresponse or
828 # raw bytes (for backwards compatibility).
828 # raw bytes (for backwards compatibility).
829 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
829 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
830 if isinstance(result, wireprototypes.bytesresponse):
830 if isinstance(result, wireprototypes.bytesresponse):
831 result = result.data
831 result = result.data
832 res.append(escapearg(result))
832 res.append(escapearg(result))
833
833
834 return wireprototypes.bytesresponse(';'.join(res))
834 return wireprototypes.bytesresponse(';'.join(res))
835
835
836 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
836 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
837 permission='pull')
837 permission='pull')
838 def between(repo, proto, pairs):
838 def between(repo, proto, pairs):
839 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
839 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
840 r = []
840 r = []
841 for b in repo.between(pairs):
841 for b in repo.between(pairs):
842 r.append(encodelist(b) + "\n")
842 r.append(encodelist(b) + "\n")
843
843
844 return wireprototypes.bytesresponse(''.join(r))
844 return wireprototypes.bytesresponse(''.join(r))
845
845
846 @wireprotocommand('branchmap', permission='pull',
846 @wireprotocommand('branchmap', permission='pull',
847 transportpolicy=POLICY_V1_ONLY)
847 transportpolicy=POLICY_V1_ONLY)
848 def branchmap(repo, proto):
848 def branchmap(repo, proto):
849 branchmap = repo.branchmap()
849 branchmap = repo.branchmap()
850 heads = []
850 heads = []
851 for branch, nodes in branchmap.iteritems():
851 for branch, nodes in branchmap.iteritems():
852 branchname = urlreq.quote(encoding.fromlocal(branch))
852 branchname = urlreq.quote(encoding.fromlocal(branch))
853 branchnodes = encodelist(nodes)
853 branchnodes = encodelist(nodes)
854 heads.append('%s %s' % (branchname, branchnodes))
854 heads.append('%s %s' % (branchname, branchnodes))
855
855
856 return wireprototypes.bytesresponse('\n'.join(heads))
856 return wireprototypes.bytesresponse('\n'.join(heads))
857
857
858 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
858 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
859 permission='pull')
859 permission='pull')
860 def branches(repo, proto, nodes):
860 def branches(repo, proto, nodes):
861 nodes = decodelist(nodes)
861 nodes = decodelist(nodes)
862 r = []
862 r = []
863 for b in repo.branches(nodes):
863 for b in repo.branches(nodes):
864 r.append(encodelist(b) + "\n")
864 r.append(encodelist(b) + "\n")
865
865
866 return wireprototypes.bytesresponse(''.join(r))
866 return wireprototypes.bytesresponse(''.join(r))
867
867
868 @wireprotocommand('clonebundles', '', permission='pull',
868 @wireprotocommand('clonebundles', '', permission='pull',
869 transportpolicy=POLICY_V1_ONLY)
869 transportpolicy=POLICY_V1_ONLY)
870 def clonebundles(repo, proto):
870 def clonebundles(repo, proto):
871 """Server command for returning info for available bundles to seed clones.
871 """Server command for returning info for available bundles to seed clones.
872
872
873 Clients will parse this response and determine what bundle to fetch.
873 Clients will parse this response and determine what bundle to fetch.
874
874
875 Extensions may wrap this command to filter or dynamically emit data
875 Extensions may wrap this command to filter or dynamically emit data
876 depending on the request. e.g. you could advertise URLs for the closest
876 depending on the request. e.g. you could advertise URLs for the closest
877 data center given the client's IP address.
877 data center given the client's IP address.
878 """
878 """
879 return wireprototypes.bytesresponse(
879 return wireprototypes.bytesresponse(
880 repo.vfs.tryread('clonebundles.manifest'))
880 repo.vfs.tryread('clonebundles.manifest'))
881
881
882 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
882 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
883 'known', 'getbundle', 'unbundlehash']
883 'known', 'getbundle', 'unbundlehash']
884
884
885 def _capabilities(repo, proto):
885 def _capabilities(repo, proto):
886 """return a list of capabilities for a repo
886 """return a list of capabilities for a repo
887
887
888 This function exists to allow extensions to easily wrap capabilities
888 This function exists to allow extensions to easily wrap capabilities
889 computation
889 computation
890
890
891 - returns a lists: easy to alter
891 - returns a lists: easy to alter
892 - change done here will be propagated to both `capabilities` and `hello`
892 - change done here will be propagated to both `capabilities` and `hello`
893 command without any other action needed.
893 command without any other action needed.
894 """
894 """
895 # copy to prevent modification of the global list
895 # copy to prevent modification of the global list
896 caps = list(wireprotocaps)
896 caps = list(wireprotocaps)
897
897
898 # Command of same name as capability isn't exposed to version 1 of
898 # Command of same name as capability isn't exposed to version 1 of
899 # transports. So conditionally add it.
899 # transports. So conditionally add it.
900 if commands.commandavailable('changegroupsubset', proto):
900 if commands.commandavailable('changegroupsubset', proto):
901 caps.append('changegroupsubset')
901 caps.append('changegroupsubset')
902
902
903 if streamclone.allowservergeneration(repo):
903 if streamclone.allowservergeneration(repo):
904 if repo.ui.configbool('server', 'preferuncompressed'):
904 if repo.ui.configbool('server', 'preferuncompressed'):
905 caps.append('stream-preferred')
905 caps.append('stream-preferred')
906 requiredformats = repo.requirements & repo.supportedformats
906 requiredformats = repo.requirements & repo.supportedformats
907 # if our local revlogs are just revlogv1, add 'stream' cap
907 # if our local revlogs are just revlogv1, add 'stream' cap
908 if not requiredformats - {'revlogv1'}:
908 if not requiredformats - {'revlogv1'}:
909 caps.append('stream')
909 caps.append('stream')
910 # otherwise, add 'streamreqs' detailing our local revlog format
910 # otherwise, add 'streamreqs' detailing our local revlog format
911 else:
911 else:
912 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
912 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
913 if repo.ui.configbool('experimental', 'bundle2-advertise'):
913 if repo.ui.configbool('experimental', 'bundle2-advertise'):
914 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
914 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
915 caps.append('bundle2=' + urlreq.quote(capsblob))
915 caps.append('bundle2=' + urlreq.quote(capsblob))
916 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
916 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
917
917
918 return proto.addcapabilities(repo, caps)
918 return proto.addcapabilities(repo, caps)
919
919
920 # If you are writing an extension and consider wrapping this function. Wrap
920 # If you are writing an extension and consider wrapping this function. Wrap
921 # `_capabilities` instead.
921 # `_capabilities` instead.
922 @wireprotocommand('capabilities', permission='pull',
922 @wireprotocommand('capabilities', permission='pull',
923 transportpolicy=POLICY_V1_ONLY)
923 transportpolicy=POLICY_V1_ONLY)
924 def capabilities(repo, proto):
924 def capabilities(repo, proto):
925 caps = _capabilities(repo, proto)
925 caps = _capabilities(repo, proto)
926 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
926 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
927
927
928 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
928 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
929 permission='pull')
929 permission='pull')
930 def changegroup(repo, proto, roots):
930 def changegroup(repo, proto, roots):
931 nodes = decodelist(roots)
931 nodes = decodelist(roots)
932 outgoing = discovery.outgoing(repo, missingroots=nodes,
932 outgoing = discovery.outgoing(repo, missingroots=nodes,
933 missingheads=repo.heads())
933 missingheads=repo.heads())
934 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
934 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
935 gen = iter(lambda: cg.read(32768), '')
935 gen = iter(lambda: cg.read(32768), '')
936 return wireprototypes.streamres(gen=gen)
936 return wireprototypes.streamres(gen=gen)
937
937
938 @wireprotocommand('changegroupsubset', 'bases heads',
938 @wireprotocommand('changegroupsubset', 'bases heads',
939 transportpolicy=POLICY_V1_ONLY,
939 transportpolicy=POLICY_V1_ONLY,
940 permission='pull')
940 permission='pull')
941 def changegroupsubset(repo, proto, bases, heads):
941 def changegroupsubset(repo, proto, bases, heads):
942 bases = decodelist(bases)
942 bases = decodelist(bases)
943 heads = decodelist(heads)
943 heads = decodelist(heads)
944 outgoing = discovery.outgoing(repo, missingroots=bases,
944 outgoing = discovery.outgoing(repo, missingroots=bases,
945 missingheads=heads)
945 missingheads=heads)
946 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
946 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
947 gen = iter(lambda: cg.read(32768), '')
947 gen = iter(lambda: cg.read(32768), '')
948 return wireprototypes.streamres(gen=gen)
948 return wireprototypes.streamres(gen=gen)
949
949
950 @wireprotocommand('debugwireargs', 'one two *',
950 @wireprotocommand('debugwireargs', 'one two *',
951 permission='pull', transportpolicy=POLICY_V1_ONLY)
951 permission='pull', transportpolicy=POLICY_V1_ONLY)
952 def debugwireargs(repo, proto, one, two, others):
952 def debugwireargs(repo, proto, one, two, others):
953 # only accept optional args from the known set
953 # only accept optional args from the known set
954 opts = options('debugwireargs', ['three', 'four'], others)
954 opts = options('debugwireargs', ['three', 'four'], others)
955 return wireprototypes.bytesresponse(repo.debugwireargs(
955 return wireprototypes.bytesresponse(repo.debugwireargs(
956 one, two, **pycompat.strkwargs(opts)))
956 one, two, **pycompat.strkwargs(opts)))
957
957
958 def find_pullbundle(repo, proto, opts, clheads, heads, common):
958 def find_pullbundle(repo, proto, opts, clheads, heads, common):
959 """Return a file object for the first matching pullbundle.
959 """Return a file object for the first matching pullbundle.
960
960
961 Pullbundles are specified in .hg/pullbundles.manifest similar to
961 Pullbundles are specified in .hg/pullbundles.manifest similar to
962 clonebundles.
962 clonebundles.
963 For each entry, the bundle specification is checked for compatibility:
963 For each entry, the bundle specification is checked for compatibility:
964 - Client features vs the BUNDLESPEC.
964 - Client features vs the BUNDLESPEC.
965 - Revisions shared with the clients vs base revisions of the bundle.
965 - Revisions shared with the clients vs base revisions of the bundle.
966 A bundle can be applied only if all its base revisions are known by
966 A bundle can be applied only if all its base revisions are known by
967 the client.
967 the client.
968 - At least one leaf of the bundle's DAG is missing on the client.
968 - At least one leaf of the bundle's DAG is missing on the client.
969 - Every leaf of the bundle's DAG is part of node set the client wants.
969 - Every leaf of the bundle's DAG is part of node set the client wants.
970 E.g. do not send a bundle of all changes if the client wants only
970 E.g. do not send a bundle of all changes if the client wants only
971 one specific branch of many.
971 one specific branch of many.
972 """
972 """
973 def decodehexstring(s):
973 def decodehexstring(s):
974 return set([h.decode('hex') for h in s.split(';')])
974 return set([h.decode('hex') for h in s.split(';')])
975
975
976 manifest = repo.vfs.tryread('pullbundles.manifest')
976 manifest = repo.vfs.tryread('pullbundles.manifest')
977 if not manifest:
977 if not manifest:
978 return None
978 return None
979 res = exchange.parseclonebundlesmanifest(repo, manifest)
979 res = exchange.parseclonebundlesmanifest(repo, manifest)
980 res = exchange.filterclonebundleentries(repo, res)
980 res = exchange.filterclonebundleentries(repo, res)
981 if not res:
981 if not res:
982 return None
982 return None
983 cl = repo.changelog
983 cl = repo.changelog
984 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
984 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
985 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
985 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
986 compformats = clientcompressionsupport(proto)
986 compformats = clientcompressionsupport(proto)
987 for entry in res:
987 for entry in res:
988 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
988 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
989 continue
989 continue
990 # No test yet for VERSION, since V2 is supported by any client
990 # No test yet for VERSION, since V2 is supported by any client
991 # that advertises partial pulls
991 # that advertises partial pulls
992 if 'heads' in entry:
992 if 'heads' in entry:
993 try:
993 try:
994 bundle_heads = decodehexstring(entry['heads'])
994 bundle_heads = decodehexstring(entry['heads'])
995 except TypeError:
995 except TypeError:
996 # Bad heads entry
996 # Bad heads entry
997 continue
997 continue
998 if bundle_heads.issubset(common):
998 if bundle_heads.issubset(common):
999 continue # Nothing new
999 continue # Nothing new
1000 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
1000 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
1001 continue # Still nothing new
1001 continue # Still nothing new
1002 if any(cl.rev(rev) not in heads_anc and
1002 if any(cl.rev(rev) not in heads_anc and
1003 cl.rev(rev) not in common_anc for rev in bundle_heads):
1003 cl.rev(rev) not in common_anc for rev in bundle_heads):
1004 continue
1004 continue
1005 if 'bases' in entry:
1005 if 'bases' in entry:
1006 try:
1006 try:
1007 bundle_bases = decodehexstring(entry['bases'])
1007 bundle_bases = decodehexstring(entry['bases'])
1008 except TypeError:
1008 except TypeError:
1009 # Bad bases entry
1009 # Bad bases entry
1010 continue
1010 continue
1011 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
1011 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
1012 continue
1012 continue
1013 path = entry['URL']
1013 path = entry['URL']
1014 repo.ui.debug('sending pullbundle "%s"\n' % path)
1014 repo.ui.debug('sending pullbundle "%s"\n' % path)
1015 try:
1015 try:
1016 return repo.vfs.open(path)
1016 return repo.vfs.open(path)
1017 except IOError:
1017 except IOError:
1018 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
1018 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
1019 continue
1019 continue
1020 return None
1020 return None
1021
1021
1022 @wireprotocommand('getbundle', '*', permission='pull')
1022 @wireprotocommand('getbundle', '*', permission='pull',
1023 transportpolicy=POLICY_V1_ONLY)
1023 def getbundle(repo, proto, others):
1024 def getbundle(repo, proto, others):
1024 opts = options('getbundle', gboptsmap.keys(), others)
1025 opts = options('getbundle', gboptsmap.keys(), others)
1025 for k, v in opts.iteritems():
1026 for k, v in opts.iteritems():
1026 keytype = gboptsmap[k]
1027 keytype = gboptsmap[k]
1027 if keytype == 'nodes':
1028 if keytype == 'nodes':
1028 opts[k] = decodelist(v)
1029 opts[k] = decodelist(v)
1029 elif keytype == 'csv':
1030 elif keytype == 'csv':
1030 opts[k] = list(v.split(','))
1031 opts[k] = list(v.split(','))
1031 elif keytype == 'scsv':
1032 elif keytype == 'scsv':
1032 opts[k] = set(v.split(','))
1033 opts[k] = set(v.split(','))
1033 elif keytype == 'boolean':
1034 elif keytype == 'boolean':
1034 # Client should serialize False as '0', which is a non-empty string
1035 # Client should serialize False as '0', which is a non-empty string
1035 # so it evaluates as a True bool.
1036 # so it evaluates as a True bool.
1036 if v == '0':
1037 if v == '0':
1037 opts[k] = False
1038 opts[k] = False
1038 else:
1039 else:
1039 opts[k] = bool(v)
1040 opts[k] = bool(v)
1040 elif keytype != 'plain':
1041 elif keytype != 'plain':
1041 raise KeyError('unknown getbundle option type %s'
1042 raise KeyError('unknown getbundle option type %s'
1042 % keytype)
1043 % keytype)
1043
1044
1044 if not bundle1allowed(repo, 'pull'):
1045 if not bundle1allowed(repo, 'pull'):
1045 if not exchange.bundle2requested(opts.get('bundlecaps')):
1046 if not exchange.bundle2requested(opts.get('bundlecaps')):
1046 if proto.name == 'http-v1':
1047 if proto.name == 'http-v1':
1047 return wireprototypes.ooberror(bundle2required)
1048 return wireprototypes.ooberror(bundle2required)
1048 raise error.Abort(bundle2requiredmain,
1049 raise error.Abort(bundle2requiredmain,
1049 hint=bundle2requiredhint)
1050 hint=bundle2requiredhint)
1050
1051
1051 prefercompressed = True
1052 prefercompressed = True
1052
1053
1053 try:
1054 try:
1054 clheads = set(repo.changelog.heads())
1055 clheads = set(repo.changelog.heads())
1055 heads = set(opts.get('heads', set()))
1056 heads = set(opts.get('heads', set()))
1056 common = set(opts.get('common', set()))
1057 common = set(opts.get('common', set()))
1057 common.discard(nullid)
1058 common.discard(nullid)
1058 if (repo.ui.configbool('server', 'pullbundle') and
1059 if (repo.ui.configbool('server', 'pullbundle') and
1059 'partial-pull' in proto.getprotocaps()):
1060 'partial-pull' in proto.getprotocaps()):
1060 # Check if a pre-built bundle covers this request.
1061 # Check if a pre-built bundle covers this request.
1061 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
1062 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
1062 if bundle:
1063 if bundle:
1063 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
1064 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
1064 prefer_uncompressed=True)
1065 prefer_uncompressed=True)
1065
1066
1066 if repo.ui.configbool('server', 'disablefullbundle'):
1067 if repo.ui.configbool('server', 'disablefullbundle'):
1067 # Check to see if this is a full clone.
1068 # Check to see if this is a full clone.
1068 changegroup = opts.get('cg', True)
1069 changegroup = opts.get('cg', True)
1069 if changegroup and not common and clheads == heads:
1070 if changegroup and not common and clheads == heads:
1070 raise error.Abort(
1071 raise error.Abort(
1071 _('server has pull-based clones disabled'),
1072 _('server has pull-based clones disabled'),
1072 hint=_('remove --pull if specified or upgrade Mercurial'))
1073 hint=_('remove --pull if specified or upgrade Mercurial'))
1073
1074
1074 info, chunks = exchange.getbundlechunks(repo, 'serve',
1075 info, chunks = exchange.getbundlechunks(repo, 'serve',
1075 **pycompat.strkwargs(opts))
1076 **pycompat.strkwargs(opts))
1076 prefercompressed = info.get('prefercompressed', True)
1077 prefercompressed = info.get('prefercompressed', True)
1077 except error.Abort as exc:
1078 except error.Abort as exc:
1078 # cleanly forward Abort error to the client
1079 # cleanly forward Abort error to the client
1079 if not exchange.bundle2requested(opts.get('bundlecaps')):
1080 if not exchange.bundle2requested(opts.get('bundlecaps')):
1080 if proto.name == 'http-v1':
1081 if proto.name == 'http-v1':
1081 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
1082 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
1082 raise # cannot do better for bundle1 + ssh
1083 raise # cannot do better for bundle1 + ssh
1083 # bundle2 request expect a bundle2 reply
1084 # bundle2 request expect a bundle2 reply
1084 bundler = bundle2.bundle20(repo.ui)
1085 bundler = bundle2.bundle20(repo.ui)
1085 manargs = [('message', pycompat.bytestr(exc))]
1086 manargs = [('message', pycompat.bytestr(exc))]
1086 advargs = []
1087 advargs = []
1087 if exc.hint is not None:
1088 if exc.hint is not None:
1088 advargs.append(('hint', exc.hint))
1089 advargs.append(('hint', exc.hint))
1089 bundler.addpart(bundle2.bundlepart('error:abort',
1090 bundler.addpart(bundle2.bundlepart('error:abort',
1090 manargs, advargs))
1091 manargs, advargs))
1091 chunks = bundler.getchunks()
1092 chunks = bundler.getchunks()
1092 prefercompressed = False
1093 prefercompressed = False
1093
1094
1094 return wireprototypes.streamres(
1095 return wireprototypes.streamres(
1095 gen=chunks, prefer_uncompressed=not prefercompressed)
1096 gen=chunks, prefer_uncompressed=not prefercompressed)
1096
1097
1097 @wireprotocommand('heads', permission='pull', transportpolicy=POLICY_V1_ONLY)
1098 @wireprotocommand('heads', permission='pull', transportpolicy=POLICY_V1_ONLY)
1098 def heads(repo, proto):
1099 def heads(repo, proto):
1099 h = repo.heads()
1100 h = repo.heads()
1100 return wireprototypes.bytesresponse(encodelist(h) + '\n')
1101 return wireprototypes.bytesresponse(encodelist(h) + '\n')
1101
1102
1102 @wireprotocommand('hello', permission='pull', transportpolicy=POLICY_V1_ONLY)
1103 @wireprotocommand('hello', permission='pull', transportpolicy=POLICY_V1_ONLY)
1103 def hello(repo, proto):
1104 def hello(repo, proto):
1104 """Called as part of SSH handshake to obtain server info.
1105 """Called as part of SSH handshake to obtain server info.
1105
1106
1106 Returns a list of lines describing interesting things about the
1107 Returns a list of lines describing interesting things about the
1107 server, in an RFC822-like format.
1108 server, in an RFC822-like format.
1108
1109
1109 Currently, the only one defined is ``capabilities``, which consists of a
1110 Currently, the only one defined is ``capabilities``, which consists of a
1110 line of space separated tokens describing server abilities:
1111 line of space separated tokens describing server abilities:
1111
1112
1112 capabilities: <token0> <token1> <token2>
1113 capabilities: <token0> <token1> <token2>
1113 """
1114 """
1114 caps = capabilities(repo, proto).data
1115 caps = capabilities(repo, proto).data
1115 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
1116 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
1116
1117
1117 @wireprotocommand('listkeys', 'namespace', permission='pull',
1118 @wireprotocommand('listkeys', 'namespace', permission='pull',
1118 transportpolicy=POLICY_V1_ONLY)
1119 transportpolicy=POLICY_V1_ONLY)
1119 def listkeys(repo, proto, namespace):
1120 def listkeys(repo, proto, namespace):
1120 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
1121 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
1121 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
1122 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
1122
1123
1123 @wireprotocommand('lookup', 'key', permission='pull',
1124 @wireprotocommand('lookup', 'key', permission='pull',
1124 transportpolicy=POLICY_V1_ONLY)
1125 transportpolicy=POLICY_V1_ONLY)
1125 def lookup(repo, proto, key):
1126 def lookup(repo, proto, key):
1126 try:
1127 try:
1127 k = encoding.tolocal(key)
1128 k = encoding.tolocal(key)
1128 n = repo.lookup(k)
1129 n = repo.lookup(k)
1129 r = hex(n)
1130 r = hex(n)
1130 success = 1
1131 success = 1
1131 except Exception as inst:
1132 except Exception as inst:
1132 r = stringutil.forcebytestr(inst)
1133 r = stringutil.forcebytestr(inst)
1133 success = 0
1134 success = 0
1134 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
1135 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
1135
1136
1136 @wireprotocommand('known', 'nodes *', permission='pull',
1137 @wireprotocommand('known', 'nodes *', permission='pull',
1137 transportpolicy=POLICY_V1_ONLY)
1138 transportpolicy=POLICY_V1_ONLY)
1138 def known(repo, proto, nodes, others):
1139 def known(repo, proto, nodes, others):
1139 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
1140 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
1140 return wireprototypes.bytesresponse(v)
1141 return wireprototypes.bytesresponse(v)
1141
1142
1142 @wireprotocommand('protocaps', 'caps', permission='pull',
1143 @wireprotocommand('protocaps', 'caps', permission='pull',
1143 transportpolicy=POLICY_V1_ONLY)
1144 transportpolicy=POLICY_V1_ONLY)
1144 def protocaps(repo, proto, caps):
1145 def protocaps(repo, proto, caps):
1145 if proto.name == wireprototypes.SSHV1:
1146 if proto.name == wireprototypes.SSHV1:
1146 proto._protocaps = set(caps.split(' '))
1147 proto._protocaps = set(caps.split(' '))
1147 return wireprototypes.bytesresponse('OK')
1148 return wireprototypes.bytesresponse('OK')
1148
1149
1149 @wireprotocommand('pushkey', 'namespace key old new', permission='push',
1150 @wireprotocommand('pushkey', 'namespace key old new', permission='push',
1150 transportpolicy=POLICY_V1_ONLY)
1151 transportpolicy=POLICY_V1_ONLY)
1151 def pushkey(repo, proto, namespace, key, old, new):
1152 def pushkey(repo, proto, namespace, key, old, new):
1152 # compatibility with pre-1.8 clients which were accidentally
1153 # compatibility with pre-1.8 clients which were accidentally
1153 # sending raw binary nodes rather than utf-8-encoded hex
1154 # sending raw binary nodes rather than utf-8-encoded hex
1154 if len(new) == 20 and stringutil.escapestr(new) != new:
1155 if len(new) == 20 and stringutil.escapestr(new) != new:
1155 # looks like it could be a binary node
1156 # looks like it could be a binary node
1156 try:
1157 try:
1157 new.decode('utf-8')
1158 new.decode('utf-8')
1158 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
1159 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
1159 except UnicodeDecodeError:
1160 except UnicodeDecodeError:
1160 pass # binary, leave unmodified
1161 pass # binary, leave unmodified
1161 else:
1162 else:
1162 new = encoding.tolocal(new) # normal path
1163 new = encoding.tolocal(new) # normal path
1163
1164
1164 with proto.mayberedirectstdio() as output:
1165 with proto.mayberedirectstdio() as output:
1165 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
1166 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
1166 encoding.tolocal(old), new) or False
1167 encoding.tolocal(old), new) or False
1167
1168
1168 output = output.getvalue() if output else ''
1169 output = output.getvalue() if output else ''
1169 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
1170 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
1170
1171
1171 @wireprotocommand('stream_out', permission='pull',
1172 @wireprotocommand('stream_out', permission='pull',
1172 transportpolicy=POLICY_V1_ONLY)
1173 transportpolicy=POLICY_V1_ONLY)
1173 def stream(repo, proto):
1174 def stream(repo, proto):
1174 '''If the server supports streaming clone, it advertises the "stream"
1175 '''If the server supports streaming clone, it advertises the "stream"
1175 capability with a value representing the version and flags of the repo
1176 capability with a value representing the version and flags of the repo
1176 it is serving. Client checks to see if it understands the format.
1177 it is serving. Client checks to see if it understands the format.
1177 '''
1178 '''
1178 return wireprototypes.streamreslegacy(
1179 return wireprototypes.streamreslegacy(
1179 streamclone.generatev1wireproto(repo))
1180 streamclone.generatev1wireproto(repo))
1180
1181
1181 @wireprotocommand('unbundle', 'heads', permission='push')
1182 @wireprotocommand('unbundle', 'heads', permission='push',
1183 transportpolicy=POLICY_V1_ONLY)
1182 def unbundle(repo, proto, heads):
1184 def unbundle(repo, proto, heads):
1183 their_heads = decodelist(heads)
1185 their_heads = decodelist(heads)
1184
1186
1185 with proto.mayberedirectstdio() as output:
1187 with proto.mayberedirectstdio() as output:
1186 try:
1188 try:
1187 exchange.check_heads(repo, their_heads, 'preparing changes')
1189 exchange.check_heads(repo, their_heads, 'preparing changes')
1188 cleanup = lambda: None
1190 cleanup = lambda: None
1189 try:
1191 try:
1190 payload = proto.getpayload()
1192 payload = proto.getpayload()
1191 if repo.ui.configbool('server', 'streamunbundle'):
1193 if repo.ui.configbool('server', 'streamunbundle'):
1192 def cleanup():
1194 def cleanup():
1193 # Ensure that the full payload is consumed, so
1195 # Ensure that the full payload is consumed, so
1194 # that the connection doesn't contain trailing garbage.
1196 # that the connection doesn't contain trailing garbage.
1195 for p in payload:
1197 for p in payload:
1196 pass
1198 pass
1197 fp = util.chunkbuffer(payload)
1199 fp = util.chunkbuffer(payload)
1198 else:
1200 else:
1199 # write bundle data to temporary file as it can be big
1201 # write bundle data to temporary file as it can be big
1200 fp, tempname = None, None
1202 fp, tempname = None, None
1201 def cleanup():
1203 def cleanup():
1202 if fp:
1204 if fp:
1203 fp.close()
1205 fp.close()
1204 if tempname:
1206 if tempname:
1205 os.unlink(tempname)
1207 os.unlink(tempname)
1206 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1208 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1207 repo.ui.debug('redirecting incoming bundle to %s\n' %
1209 repo.ui.debug('redirecting incoming bundle to %s\n' %
1208 tempname)
1210 tempname)
1209 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
1211 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
1210 r = 0
1212 r = 0
1211 for p in payload:
1213 for p in payload:
1212 fp.write(p)
1214 fp.write(p)
1213 fp.seek(0)
1215 fp.seek(0)
1214
1216
1215 gen = exchange.readbundle(repo.ui, fp, None)
1217 gen = exchange.readbundle(repo.ui, fp, None)
1216 if (isinstance(gen, changegroupmod.cg1unpacker)
1218 if (isinstance(gen, changegroupmod.cg1unpacker)
1217 and not bundle1allowed(repo, 'push')):
1219 and not bundle1allowed(repo, 'push')):
1218 if proto.name == 'http-v1':
1220 if proto.name == 'http-v1':
1219 # need to special case http because stderr do not get to
1221 # need to special case http because stderr do not get to
1220 # the http client on failed push so we need to abuse
1222 # the http client on failed push so we need to abuse
1221 # some other error type to make sure the message get to
1223 # some other error type to make sure the message get to
1222 # the user.
1224 # the user.
1223 return wireprototypes.ooberror(bundle2required)
1225 return wireprototypes.ooberror(bundle2required)
1224 raise error.Abort(bundle2requiredmain,
1226 raise error.Abort(bundle2requiredmain,
1225 hint=bundle2requiredhint)
1227 hint=bundle2requiredhint)
1226
1228
1227 r = exchange.unbundle(repo, gen, their_heads, 'serve',
1229 r = exchange.unbundle(repo, gen, their_heads, 'serve',
1228 proto.client())
1230 proto.client())
1229 if util.safehasattr(r, 'addpart'):
1231 if util.safehasattr(r, 'addpart'):
1230 # The return looks streamable, we are in the bundle2 case
1232 # The return looks streamable, we are in the bundle2 case
1231 # and should return a stream.
1233 # and should return a stream.
1232 return wireprototypes.streamreslegacy(gen=r.getchunks())
1234 return wireprototypes.streamreslegacy(gen=r.getchunks())
1233 return wireprototypes.pushres(
1235 return wireprototypes.pushres(
1234 r, output.getvalue() if output else '')
1236 r, output.getvalue() if output else '')
1235
1237
1236 finally:
1238 finally:
1237 cleanup()
1239 cleanup()
1238
1240
1239 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1241 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1240 # handle non-bundle2 case first
1242 # handle non-bundle2 case first
1241 if not getattr(exc, 'duringunbundle2', False):
1243 if not getattr(exc, 'duringunbundle2', False):
1242 try:
1244 try:
1243 raise
1245 raise
1244 except error.Abort:
1246 except error.Abort:
1245 # The old code we moved used procutil.stderr directly.
1247 # The old code we moved used procutil.stderr directly.
1246 # We did not change it to minimise code change.
1248 # We did not change it to minimise code change.
1247 # This need to be moved to something proper.
1249 # This need to be moved to something proper.
1248 # Feel free to do it.
1250 # Feel free to do it.
1249 procutil.stderr.write("abort: %s\n" % exc)
1251 procutil.stderr.write("abort: %s\n" % exc)
1250 if exc.hint is not None:
1252 if exc.hint is not None:
1251 procutil.stderr.write("(%s)\n" % exc.hint)
1253 procutil.stderr.write("(%s)\n" % exc.hint)
1252 procutil.stderr.flush()
1254 procutil.stderr.flush()
1253 return wireprototypes.pushres(
1255 return wireprototypes.pushres(
1254 0, output.getvalue() if output else '')
1256 0, output.getvalue() if output else '')
1255 except error.PushRaced:
1257 except error.PushRaced:
1256 return wireprototypes.pusherr(
1258 return wireprototypes.pusherr(
1257 pycompat.bytestr(exc),
1259 pycompat.bytestr(exc),
1258 output.getvalue() if output else '')
1260 output.getvalue() if output else '')
1259
1261
1260 bundler = bundle2.bundle20(repo.ui)
1262 bundler = bundle2.bundle20(repo.ui)
1261 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1263 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1262 bundler.addpart(out)
1264 bundler.addpart(out)
1263 try:
1265 try:
1264 try:
1266 try:
1265 raise
1267 raise
1266 except error.PushkeyFailed as exc:
1268 except error.PushkeyFailed as exc:
1267 # check client caps
1269 # check client caps
1268 remotecaps = getattr(exc, '_replycaps', None)
1270 remotecaps = getattr(exc, '_replycaps', None)
1269 if (remotecaps is not None
1271 if (remotecaps is not None
1270 and 'pushkey' not in remotecaps.get('error', ())):
1272 and 'pushkey' not in remotecaps.get('error', ())):
1271 # no support remote side, fallback to Abort handler.
1273 # no support remote side, fallback to Abort handler.
1272 raise
1274 raise
1273 part = bundler.newpart('error:pushkey')
1275 part = bundler.newpart('error:pushkey')
1274 part.addparam('in-reply-to', exc.partid)
1276 part.addparam('in-reply-to', exc.partid)
1275 if exc.namespace is not None:
1277 if exc.namespace is not None:
1276 part.addparam('namespace', exc.namespace,
1278 part.addparam('namespace', exc.namespace,
1277 mandatory=False)
1279 mandatory=False)
1278 if exc.key is not None:
1280 if exc.key is not None:
1279 part.addparam('key', exc.key, mandatory=False)
1281 part.addparam('key', exc.key, mandatory=False)
1280 if exc.new is not None:
1282 if exc.new is not None:
1281 part.addparam('new', exc.new, mandatory=False)
1283 part.addparam('new', exc.new, mandatory=False)
1282 if exc.old is not None:
1284 if exc.old is not None:
1283 part.addparam('old', exc.old, mandatory=False)
1285 part.addparam('old', exc.old, mandatory=False)
1284 if exc.ret is not None:
1286 if exc.ret is not None:
1285 part.addparam('ret', exc.ret, mandatory=False)
1287 part.addparam('ret', exc.ret, mandatory=False)
1286 except error.BundleValueError as exc:
1288 except error.BundleValueError as exc:
1287 errpart = bundler.newpart('error:unsupportedcontent')
1289 errpart = bundler.newpart('error:unsupportedcontent')
1288 if exc.parttype is not None:
1290 if exc.parttype is not None:
1289 errpart.addparam('parttype', exc.parttype)
1291 errpart.addparam('parttype', exc.parttype)
1290 if exc.params:
1292 if exc.params:
1291 errpart.addparam('params', '\0'.join(exc.params))
1293 errpart.addparam('params', '\0'.join(exc.params))
1292 except error.Abort as exc:
1294 except error.Abort as exc:
1293 manargs = [('message', stringutil.forcebytestr(exc))]
1295 manargs = [('message', stringutil.forcebytestr(exc))]
1294 advargs = []
1296 advargs = []
1295 if exc.hint is not None:
1297 if exc.hint is not None:
1296 advargs.append(('hint', exc.hint))
1298 advargs.append(('hint', exc.hint))
1297 bundler.addpart(bundle2.bundlepart('error:abort',
1299 bundler.addpart(bundle2.bundlepart('error:abort',
1298 manargs, advargs))
1300 manargs, advargs))
1299 except error.PushRaced as exc:
1301 except error.PushRaced as exc:
1300 bundler.newpart('error:pushraced',
1302 bundler.newpart('error:pushraced',
1301 [('message', stringutil.forcebytestr(exc))])
1303 [('message', stringutil.forcebytestr(exc))])
1302 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
1304 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
1303
1305
1304 # Wire protocol version 2 commands only past this point.
1306 # Wire protocol version 2 commands only past this point.
1305
1307
1306 def _capabilitiesv2(repo, proto):
1308 def _capabilitiesv2(repo, proto):
1307 """Obtain the set of capabilities for version 2 transports.
1309 """Obtain the set of capabilities for version 2 transports.
1308
1310
1309 These capabilities are distinct from the capabilities for version 1
1311 These capabilities are distinct from the capabilities for version 1
1310 transports.
1312 transports.
1311 """
1313 """
1312 compression = []
1314 compression = []
1313 for engine in supportedcompengines(repo.ui, util.SERVERROLE):
1315 for engine in supportedcompengines(repo.ui, util.SERVERROLE):
1314 compression.append({
1316 compression.append({
1315 b'name': engine.wireprotosupport().name,
1317 b'name': engine.wireprotosupport().name,
1316 })
1318 })
1317
1319
1318 caps = {
1320 caps = {
1319 'commands': {},
1321 'commands': {},
1320 'compression': compression,
1322 'compression': compression,
1321 }
1323 }
1322
1324
1323 for command, entry in commandsv2.items():
1325 for command, entry in commandsv2.items():
1324 caps['commands'][command] = {
1326 caps['commands'][command] = {
1325 'args': entry.args,
1327 'args': entry.args,
1326 'permissions': [entry.permission],
1328 'permissions': [entry.permission],
1327 }
1329 }
1328
1330
1329 return proto.addcapabilities(repo, caps)
1331 return proto.addcapabilities(repo, caps)
1330
1332
1331 @wireprotocommand('branchmap', permission='pull',
1333 @wireprotocommand('branchmap', permission='pull',
1332 transportpolicy=POLICY_V2_ONLY)
1334 transportpolicy=POLICY_V2_ONLY)
1333 def branchmapv2(repo, proto):
1335 def branchmapv2(repo, proto):
1334 branchmap = {encoding.fromlocal(k): v
1336 branchmap = {encoding.fromlocal(k): v
1335 for k, v in repo.branchmap().iteritems()}
1337 for k, v in repo.branchmap().iteritems()}
1336
1338
1337 return wireprototypes.cborresponse(branchmap)
1339 return wireprototypes.cborresponse(branchmap)
1338
1340
1339 @wireprotocommand('capabilities', permission='pull',
1341 @wireprotocommand('capabilities', permission='pull',
1340 transportpolicy=POLICY_V2_ONLY)
1342 transportpolicy=POLICY_V2_ONLY)
1341 def capabilitiesv2(repo, proto):
1343 def capabilitiesv2(repo, proto):
1342 caps = _capabilitiesv2(repo, proto)
1344 caps = _capabilitiesv2(repo, proto)
1343
1345
1344 return wireprototypes.cborresponse(caps)
1346 return wireprototypes.cborresponse(caps)
1345
1347
1346 @wireprotocommand('heads',
1348 @wireprotocommand('heads',
1347 args={
1349 args={
1348 'publiconly': False,
1350 'publiconly': False,
1349 },
1351 },
1350 permission='pull',
1352 permission='pull',
1351 transportpolicy=POLICY_V2_ONLY)
1353 transportpolicy=POLICY_V2_ONLY)
1352 def headsv2(repo, proto, publiconly=False):
1354 def headsv2(repo, proto, publiconly=False):
1353 if publiconly:
1355 if publiconly:
1354 repo = repo.filtered('immutable')
1356 repo = repo.filtered('immutable')
1355
1357
1356 return wireprototypes.cborresponse(repo.heads())
1358 return wireprototypes.cborresponse(repo.heads())
1357
1359
1358 @wireprotocommand('known',
1360 @wireprotocommand('known',
1359 args={
1361 args={
1360 'nodes': [b'deadbeef'],
1362 'nodes': [b'deadbeef'],
1361 },
1363 },
1362 permission='pull',
1364 permission='pull',
1363 transportpolicy=POLICY_V2_ONLY)
1365 transportpolicy=POLICY_V2_ONLY)
1364 def knownv2(repo, proto, nodes=None):
1366 def knownv2(repo, proto, nodes=None):
1365 nodes = nodes or []
1367 nodes = nodes or []
1366 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1368 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1367 return wireprototypes.cborresponse(result)
1369 return wireprototypes.cborresponse(result)
1368
1370
1369 @wireprotocommand('listkeys',
1371 @wireprotocommand('listkeys',
1370 args={
1372 args={
1371 'namespace': b'ns',
1373 'namespace': b'ns',
1372 },
1374 },
1373 permission='pull',
1375 permission='pull',
1374 transportpolicy=POLICY_V2_ONLY)
1376 transportpolicy=POLICY_V2_ONLY)
1375 def listkeysv2(repo, proto, namespace=None):
1377 def listkeysv2(repo, proto, namespace=None):
1376 keys = repo.listkeys(encoding.tolocal(namespace))
1378 keys = repo.listkeys(encoding.tolocal(namespace))
1377 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
1379 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
1378 for k, v in keys.iteritems()}
1380 for k, v in keys.iteritems()}
1379
1381
1380 return wireprototypes.cborresponse(keys)
1382 return wireprototypes.cborresponse(keys)
1381
1383
1382 @wireprotocommand('lookup',
1384 @wireprotocommand('lookup',
1383 args={
1385 args={
1384 'key': b'foo',
1386 'key': b'foo',
1385 },
1387 },
1386 permission='pull',
1388 permission='pull',
1387 transportpolicy=POLICY_V2_ONLY)
1389 transportpolicy=POLICY_V2_ONLY)
1388 def lookupv2(repo, proto, key):
1390 def lookupv2(repo, proto, key):
1389 key = encoding.tolocal(key)
1391 key = encoding.tolocal(key)
1390
1392
1391 # TODO handle exception.
1393 # TODO handle exception.
1392 node = repo.lookup(key)
1394 node = repo.lookup(key)
1393
1395
1394 return wireprototypes.cborresponse(node)
1396 return wireprototypes.cborresponse(node)
1395
1397
1396 @wireprotocommand('pushkey',
1398 @wireprotocommand('pushkey',
1397 args={
1399 args={
1398 'namespace': b'ns',
1400 'namespace': b'ns',
1399 'key': b'key',
1401 'key': b'key',
1400 'old': b'old',
1402 'old': b'old',
1401 'new': b'new',
1403 'new': b'new',
1402 },
1404 },
1403 permission='push',
1405 permission='push',
1404 transportpolicy=POLICY_V2_ONLY)
1406 transportpolicy=POLICY_V2_ONLY)
1405 def pushkeyv2(repo, proto, namespace, key, old, new):
1407 def pushkeyv2(repo, proto, namespace, key, old, new):
1406 # TODO handle ui output redirection
1408 # TODO handle ui output redirection
1407 r = repo.pushkey(encoding.tolocal(namespace),
1409 r = repo.pushkey(encoding.tolocal(namespace),
1408 encoding.tolocal(key),
1410 encoding.tolocal(key),
1409 encoding.tolocal(old),
1411 encoding.tolocal(old),
1410 encoding.tolocal(new))
1412 encoding.tolocal(new))
1411
1413
1412 return wireprototypes.cborresponse(r)
1414 return wireprototypes.cborresponse(r)
@@ -1,564 +1,564 b''
1 $ . $TESTDIR/wireprotohelpers.sh
1 $ . $TESTDIR/wireprotohelpers.sh
2 $ enabledummycommands
2 $ enabledummycommands
3
3
4 $ hg init server
4 $ hg init server
5 $ cat > server/.hg/hgrc << EOF
5 $ cat > server/.hg/hgrc << EOF
6 > [experimental]
6 > [experimental]
7 > web.apiserver = true
7 > web.apiserver = true
8 > EOF
8 > EOF
9 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
9 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
10 $ cat hg.pid > $DAEMON_PIDS
10 $ cat hg.pid > $DAEMON_PIDS
11
11
12 HTTP v2 protocol not enabled by default
12 HTTP v2 protocol not enabled by default
13
13
14 $ sendhttpraw << EOF
14 $ sendhttpraw << EOF
15 > httprequest GET api/$HTTPV2
15 > httprequest GET api/$HTTPV2
16 > user-agent: test
16 > user-agent: test
17 > EOF
17 > EOF
18 using raw connection to peer
18 using raw connection to peer
19 s> GET /api/exp-http-v2-0001 HTTP/1.1\r\n
19 s> GET /api/exp-http-v2-0001 HTTP/1.1\r\n
20 s> Accept-Encoding: identity\r\n
20 s> Accept-Encoding: identity\r\n
21 s> user-agent: test\r\n
21 s> user-agent: test\r\n
22 s> host: $LOCALIP:$HGPORT\r\n (glob)
22 s> host: $LOCALIP:$HGPORT\r\n (glob)
23 s> \r\n
23 s> \r\n
24 s> makefile('rb', None)
24 s> makefile('rb', None)
25 s> HTTP/1.1 404 Not Found\r\n
25 s> HTTP/1.1 404 Not Found\r\n
26 s> Server: testing stub value\r\n
26 s> Server: testing stub value\r\n
27 s> Date: $HTTP_DATE$\r\n
27 s> Date: $HTTP_DATE$\r\n
28 s> Content-Type: text/plain\r\n
28 s> Content-Type: text/plain\r\n
29 s> Content-Length: 33\r\n
29 s> Content-Length: 33\r\n
30 s> \r\n
30 s> \r\n
31 s> API exp-http-v2-0001 not enabled\n
31 s> API exp-http-v2-0001 not enabled\n
32
32
33 Restart server with support for HTTP v2 API
33 Restart server with support for HTTP v2 API
34
34
35 $ killdaemons.py
35 $ killdaemons.py
36 $ enablehttpv2 server
36 $ enablehttpv2 server
37 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
37 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
38 $ cat hg.pid > $DAEMON_PIDS
38 $ cat hg.pid > $DAEMON_PIDS
39
39
40 Request to unknown command yields 404
40 Request to unknown command yields 404
41
41
42 $ sendhttpraw << EOF
42 $ sendhttpraw << EOF
43 > httprequest POST api/$HTTPV2/ro/badcommand
43 > httprequest POST api/$HTTPV2/ro/badcommand
44 > user-agent: test
44 > user-agent: test
45 > EOF
45 > EOF
46 using raw connection to peer
46 using raw connection to peer
47 s> POST /api/exp-http-v2-0001/ro/badcommand HTTP/1.1\r\n
47 s> POST /api/exp-http-v2-0001/ro/badcommand HTTP/1.1\r\n
48 s> Accept-Encoding: identity\r\n
48 s> Accept-Encoding: identity\r\n
49 s> user-agent: test\r\n
49 s> user-agent: test\r\n
50 s> host: $LOCALIP:$HGPORT\r\n (glob)
50 s> host: $LOCALIP:$HGPORT\r\n (glob)
51 s> \r\n
51 s> \r\n
52 s> makefile('rb', None)
52 s> makefile('rb', None)
53 s> HTTP/1.1 404 Not Found\r\n
53 s> HTTP/1.1 404 Not Found\r\n
54 s> Server: testing stub value\r\n
54 s> Server: testing stub value\r\n
55 s> Date: $HTTP_DATE$\r\n
55 s> Date: $HTTP_DATE$\r\n
56 s> Content-Type: text/plain\r\n
56 s> Content-Type: text/plain\r\n
57 s> Content-Length: 42\r\n
57 s> Content-Length: 42\r\n
58 s> \r\n
58 s> \r\n
59 s> unknown wire protocol command: badcommand\n
59 s> unknown wire protocol command: badcommand\n
60
60
61 GET to read-only command yields a 405
61 GET to read-only command yields a 405
62
62
63 $ sendhttpraw << EOF
63 $ sendhttpraw << EOF
64 > httprequest GET api/$HTTPV2/ro/customreadonly
64 > httprequest GET api/$HTTPV2/ro/customreadonly
65 > user-agent: test
65 > user-agent: test
66 > EOF
66 > EOF
67 using raw connection to peer
67 using raw connection to peer
68 s> GET /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
68 s> GET /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
69 s> Accept-Encoding: identity\r\n
69 s> Accept-Encoding: identity\r\n
70 s> user-agent: test\r\n
70 s> user-agent: test\r\n
71 s> host: $LOCALIP:$HGPORT\r\n (glob)
71 s> host: $LOCALIP:$HGPORT\r\n (glob)
72 s> \r\n
72 s> \r\n
73 s> makefile('rb', None)
73 s> makefile('rb', None)
74 s> HTTP/1.1 405 Method Not Allowed\r\n
74 s> HTTP/1.1 405 Method Not Allowed\r\n
75 s> Server: testing stub value\r\n
75 s> Server: testing stub value\r\n
76 s> Date: $HTTP_DATE$\r\n
76 s> Date: $HTTP_DATE$\r\n
77 s> Allow: POST\r\n
77 s> Allow: POST\r\n
78 s> Content-Length: 30\r\n
78 s> Content-Length: 30\r\n
79 s> \r\n
79 s> \r\n
80 s> commands require POST requests
80 s> commands require POST requests
81
81
82 Missing Accept header results in 406
82 Missing Accept header results in 406
83
83
84 $ sendhttpraw << EOF
84 $ sendhttpraw << EOF
85 > httprequest POST api/$HTTPV2/ro/customreadonly
85 > httprequest POST api/$HTTPV2/ro/customreadonly
86 > user-agent: test
86 > user-agent: test
87 > EOF
87 > EOF
88 using raw connection to peer
88 using raw connection to peer
89 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
89 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
90 s> Accept-Encoding: identity\r\n
90 s> Accept-Encoding: identity\r\n
91 s> user-agent: test\r\n
91 s> user-agent: test\r\n
92 s> host: $LOCALIP:$HGPORT\r\n (glob)
92 s> host: $LOCALIP:$HGPORT\r\n (glob)
93 s> \r\n
93 s> \r\n
94 s> makefile('rb', None)
94 s> makefile('rb', None)
95 s> HTTP/1.1 406 Not Acceptable\r\n
95 s> HTTP/1.1 406 Not Acceptable\r\n
96 s> Server: testing stub value\r\n
96 s> Server: testing stub value\r\n
97 s> Date: $HTTP_DATE$\r\n
97 s> Date: $HTTP_DATE$\r\n
98 s> Content-Type: text/plain\r\n
98 s> Content-Type: text/plain\r\n
99 s> Content-Length: 85\r\n
99 s> Content-Length: 85\r\n
100 s> \r\n
100 s> \r\n
101 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0003\n
101 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0003\n
102
102
103 Bad Accept header results in 406
103 Bad Accept header results in 406
104
104
105 $ sendhttpraw << EOF
105 $ sendhttpraw << EOF
106 > httprequest POST api/$HTTPV2/ro/customreadonly
106 > httprequest POST api/$HTTPV2/ro/customreadonly
107 > accept: invalid
107 > accept: invalid
108 > user-agent: test
108 > user-agent: test
109 > EOF
109 > EOF
110 using raw connection to peer
110 using raw connection to peer
111 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
111 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
112 s> Accept-Encoding: identity\r\n
112 s> Accept-Encoding: identity\r\n
113 s> accept: invalid\r\n
113 s> accept: invalid\r\n
114 s> user-agent: test\r\n
114 s> user-agent: test\r\n
115 s> host: $LOCALIP:$HGPORT\r\n (glob)
115 s> host: $LOCALIP:$HGPORT\r\n (glob)
116 s> \r\n
116 s> \r\n
117 s> makefile('rb', None)
117 s> makefile('rb', None)
118 s> HTTP/1.1 406 Not Acceptable\r\n
118 s> HTTP/1.1 406 Not Acceptable\r\n
119 s> Server: testing stub value\r\n
119 s> Server: testing stub value\r\n
120 s> Date: $HTTP_DATE$\r\n
120 s> Date: $HTTP_DATE$\r\n
121 s> Content-Type: text/plain\r\n
121 s> Content-Type: text/plain\r\n
122 s> Content-Length: 85\r\n
122 s> Content-Length: 85\r\n
123 s> \r\n
123 s> \r\n
124 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0003\n
124 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0003\n
125
125
126 Bad Content-Type header results in 415
126 Bad Content-Type header results in 415
127
127
128 $ sendhttpraw << EOF
128 $ sendhttpraw << EOF
129 > httprequest POST api/$HTTPV2/ro/customreadonly
129 > httprequest POST api/$HTTPV2/ro/customreadonly
130 > accept: $MEDIATYPE
130 > accept: $MEDIATYPE
131 > user-agent: test
131 > user-agent: test
132 > content-type: badmedia
132 > content-type: badmedia
133 > EOF
133 > EOF
134 using raw connection to peer
134 using raw connection to peer
135 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
135 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
136 s> Accept-Encoding: identity\r\n
136 s> Accept-Encoding: identity\r\n
137 s> accept: application/mercurial-exp-framing-0003\r\n
137 s> accept: application/mercurial-exp-framing-0003\r\n
138 s> content-type: badmedia\r\n
138 s> content-type: badmedia\r\n
139 s> user-agent: test\r\n
139 s> user-agent: test\r\n
140 s> host: $LOCALIP:$HGPORT\r\n (glob)
140 s> host: $LOCALIP:$HGPORT\r\n (glob)
141 s> \r\n
141 s> \r\n
142 s> makefile('rb', None)
142 s> makefile('rb', None)
143 s> HTTP/1.1 415 Unsupported Media Type\r\n
143 s> HTTP/1.1 415 Unsupported Media Type\r\n
144 s> Server: testing stub value\r\n
144 s> Server: testing stub value\r\n
145 s> Date: $HTTP_DATE$\r\n
145 s> Date: $HTTP_DATE$\r\n
146 s> Content-Type: text/plain\r\n
146 s> Content-Type: text/plain\r\n
147 s> Content-Length: 88\r\n
147 s> Content-Length: 88\r\n
148 s> \r\n
148 s> \r\n
149 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0003\n
149 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0003\n
150
150
151 Request to read-only command works out of the box
151 Request to read-only command works out of the box
152
152
153 $ sendhttpraw << EOF
153 $ sendhttpraw << EOF
154 > httprequest POST api/$HTTPV2/ro/customreadonly
154 > httprequest POST api/$HTTPV2/ro/customreadonly
155 > accept: $MEDIATYPE
155 > accept: $MEDIATYPE
156 > content-type: $MEDIATYPE
156 > content-type: $MEDIATYPE
157 > user-agent: test
157 > user-agent: test
158 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
158 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
159 > EOF
159 > EOF
160 using raw connection to peer
160 using raw connection to peer
161 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
161 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
162 s> Accept-Encoding: identity\r\n
162 s> Accept-Encoding: identity\r\n
163 s> *\r\n (glob)
163 s> *\r\n (glob)
164 s> content-type: application/mercurial-exp-framing-0003\r\n
164 s> content-type: application/mercurial-exp-framing-0003\r\n
165 s> user-agent: test\r\n
165 s> user-agent: test\r\n
166 s> content-length: 29\r\n
166 s> content-length: 29\r\n
167 s> host: $LOCALIP:$HGPORT\r\n (glob)
167 s> host: $LOCALIP:$HGPORT\r\n (glob)
168 s> \r\n
168 s> \r\n
169 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
169 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
170 s> makefile('rb', None)
170 s> makefile('rb', None)
171 s> HTTP/1.1 200 OK\r\n
171 s> HTTP/1.1 200 OK\r\n
172 s> Server: testing stub value\r\n
172 s> Server: testing stub value\r\n
173 s> Date: $HTTP_DATE$\r\n
173 s> Date: $HTTP_DATE$\r\n
174 s> Content-Type: application/mercurial-exp-framing-0003\r\n
174 s> Content-Type: application/mercurial-exp-framing-0003\r\n
175 s> Transfer-Encoding: chunked\r\n
175 s> Transfer-Encoding: chunked\r\n
176 s> \r\n
176 s> \r\n
177 s> 25\r\n
177 s> 25\r\n
178 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
178 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
179 s> \r\n
179 s> \r\n
180 s> 0\r\n
180 s> 0\r\n
181 s> \r\n
181 s> \r\n
182
182
183 $ sendhttpv2peer << EOF
183 $ sendhttpv2peer << EOF
184 > command customreadonly
184 > command customreadonly
185 > EOF
185 > EOF
186 creating http peer for wire protocol version 2
186 creating http peer for wire protocol version 2
187 sending customreadonly command
187 sending customreadonly command
188 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
188 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
189 s> Accept-Encoding: identity\r\n
189 s> Accept-Encoding: identity\r\n
190 s> accept: application/mercurial-exp-framing-0003\r\n
190 s> accept: application/mercurial-exp-framing-0003\r\n
191 s> content-type: application/mercurial-exp-framing-0003\r\n
191 s> content-type: application/mercurial-exp-framing-0003\r\n
192 s> content-length: 29\r\n
192 s> content-length: 29\r\n
193 s> host: $LOCALIP:$HGPORT\r\n (glob)
193 s> host: $LOCALIP:$HGPORT\r\n (glob)
194 s> user-agent: Mercurial debugwireproto\r\n
194 s> user-agent: Mercurial debugwireproto\r\n
195 s> \r\n
195 s> \r\n
196 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
196 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
197 s> makefile('rb', None)
197 s> makefile('rb', None)
198 s> HTTP/1.1 200 OK\r\n
198 s> HTTP/1.1 200 OK\r\n
199 s> Server: testing stub value\r\n
199 s> Server: testing stub value\r\n
200 s> Date: $HTTP_DATE$\r\n
200 s> Date: $HTTP_DATE$\r\n
201 s> Content-Type: application/mercurial-exp-framing-0003\r\n
201 s> Content-Type: application/mercurial-exp-framing-0003\r\n
202 s> Transfer-Encoding: chunked\r\n
202 s> Transfer-Encoding: chunked\r\n
203 s> \r\n
203 s> \r\n
204 s> 25\r\n
204 s> 25\r\n
205 s> \x1d\x00\x00\x01\x00\x02\x01B
205 s> \x1d\x00\x00\x01\x00\x02\x01B
206 s> customreadonly bytes response
206 s> customreadonly bytes response
207 s> \r\n
207 s> \r\n
208 received frame(size=29; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos)
208 received frame(size=29; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos)
209 s> 0\r\n
209 s> 0\r\n
210 s> \r\n
210 s> \r\n
211 response: [b'customreadonly bytes response']
211 response: [b'customreadonly bytes response']
212
212
213 Request to read-write command fails because server is read-only by default
213 Request to read-write command fails because server is read-only by default
214
214
215 GET to read-write request yields 405
215 GET to read-write request yields 405
216
216
217 $ sendhttpraw << EOF
217 $ sendhttpraw << EOF
218 > httprequest GET api/$HTTPV2/rw/customreadonly
218 > httprequest GET api/$HTTPV2/rw/customreadonly
219 > user-agent: test
219 > user-agent: test
220 > EOF
220 > EOF
221 using raw connection to peer
221 using raw connection to peer
222 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
222 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
223 s> Accept-Encoding: identity\r\n
223 s> Accept-Encoding: identity\r\n
224 s> user-agent: test\r\n
224 s> user-agent: test\r\n
225 s> host: $LOCALIP:$HGPORT\r\n (glob)
225 s> host: $LOCALIP:$HGPORT\r\n (glob)
226 s> \r\n
226 s> \r\n
227 s> makefile('rb', None)
227 s> makefile('rb', None)
228 s> HTTP/1.1 405 Method Not Allowed\r\n
228 s> HTTP/1.1 405 Method Not Allowed\r\n
229 s> Server: testing stub value\r\n
229 s> Server: testing stub value\r\n
230 s> Date: $HTTP_DATE$\r\n
230 s> Date: $HTTP_DATE$\r\n
231 s> Allow: POST\r\n
231 s> Allow: POST\r\n
232 s> Content-Length: 30\r\n
232 s> Content-Length: 30\r\n
233 s> \r\n
233 s> \r\n
234 s> commands require POST requests
234 s> commands require POST requests
235
235
236 Even for unknown commands
236 Even for unknown commands
237
237
238 $ sendhttpraw << EOF
238 $ sendhttpraw << EOF
239 > httprequest GET api/$HTTPV2/rw/badcommand
239 > httprequest GET api/$HTTPV2/rw/badcommand
240 > user-agent: test
240 > user-agent: test
241 > EOF
241 > EOF
242 using raw connection to peer
242 using raw connection to peer
243 s> GET /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
243 s> GET /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
244 s> Accept-Encoding: identity\r\n
244 s> Accept-Encoding: identity\r\n
245 s> user-agent: test\r\n
245 s> user-agent: test\r\n
246 s> host: $LOCALIP:$HGPORT\r\n (glob)
246 s> host: $LOCALIP:$HGPORT\r\n (glob)
247 s> \r\n
247 s> \r\n
248 s> makefile('rb', None)
248 s> makefile('rb', None)
249 s> HTTP/1.1 405 Method Not Allowed\r\n
249 s> HTTP/1.1 405 Method Not Allowed\r\n
250 s> Server: testing stub value\r\n
250 s> Server: testing stub value\r\n
251 s> Date: $HTTP_DATE$\r\n
251 s> Date: $HTTP_DATE$\r\n
252 s> Allow: POST\r\n
252 s> Allow: POST\r\n
253 s> Content-Length: 30\r\n
253 s> Content-Length: 30\r\n
254 s> \r\n
254 s> \r\n
255 s> commands require POST requests
255 s> commands require POST requests
256
256
257 SSL required by default
257 SSL required by default
258
258
259 $ sendhttpraw << EOF
259 $ sendhttpraw << EOF
260 > httprequest POST api/$HTTPV2/rw/customreadonly
260 > httprequest POST api/$HTTPV2/rw/customreadonly
261 > user-agent: test
261 > user-agent: test
262 > EOF
262 > EOF
263 using raw connection to peer
263 using raw connection to peer
264 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
264 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
265 s> Accept-Encoding: identity\r\n
265 s> Accept-Encoding: identity\r\n
266 s> user-agent: test\r\n
266 s> user-agent: test\r\n
267 s> host: $LOCALIP:$HGPORT\r\n (glob)
267 s> host: $LOCALIP:$HGPORT\r\n (glob)
268 s> \r\n
268 s> \r\n
269 s> makefile('rb', None)
269 s> makefile('rb', None)
270 s> HTTP/1.1 403 ssl required\r\n
270 s> HTTP/1.1 403 ssl required\r\n
271 s> Server: testing stub value\r\n
271 s> Server: testing stub value\r\n
272 s> Date: $HTTP_DATE$\r\n
272 s> Date: $HTTP_DATE$\r\n
273 s> Content-Length: 17\r\n
273 s> Content-Length: 17\r\n
274 s> \r\n
274 s> \r\n
275 s> permission denied
275 s> permission denied
276
276
277 Restart server to allow non-ssl read-write operations
277 Restart server to allow non-ssl read-write operations
278
278
279 $ killdaemons.py
279 $ killdaemons.py
280 $ cat > server/.hg/hgrc << EOF
280 $ cat > server/.hg/hgrc << EOF
281 > [experimental]
281 > [experimental]
282 > web.apiserver = true
282 > web.apiserver = true
283 > web.api.http-v2 = true
283 > web.api.http-v2 = true
284 > [web]
284 > [web]
285 > push_ssl = false
285 > push_ssl = false
286 > allow-push = *
286 > allow-push = *
287 > EOF
287 > EOF
288
288
289 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
289 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
290 $ cat hg.pid > $DAEMON_PIDS
290 $ cat hg.pid > $DAEMON_PIDS
291
291
292 Authorized request for valid read-write command works
292 Authorized request for valid read-write command works
293
293
294 $ sendhttpraw << EOF
294 $ sendhttpraw << EOF
295 > httprequest POST api/$HTTPV2/rw/customreadonly
295 > httprequest POST api/$HTTPV2/rw/customreadonly
296 > user-agent: test
296 > user-agent: test
297 > accept: $MEDIATYPE
297 > accept: $MEDIATYPE
298 > content-type: $MEDIATYPE
298 > content-type: $MEDIATYPE
299 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
299 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
300 > EOF
300 > EOF
301 using raw connection to peer
301 using raw connection to peer
302 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
302 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
303 s> Accept-Encoding: identity\r\n
303 s> Accept-Encoding: identity\r\n
304 s> accept: application/mercurial-exp-framing-0003\r\n
304 s> accept: application/mercurial-exp-framing-0003\r\n
305 s> content-type: application/mercurial-exp-framing-0003\r\n
305 s> content-type: application/mercurial-exp-framing-0003\r\n
306 s> user-agent: test\r\n
306 s> user-agent: test\r\n
307 s> content-length: 29\r\n
307 s> content-length: 29\r\n
308 s> host: $LOCALIP:$HGPORT\r\n (glob)
308 s> host: $LOCALIP:$HGPORT\r\n (glob)
309 s> \r\n
309 s> \r\n
310 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
310 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
311 s> makefile('rb', None)
311 s> makefile('rb', None)
312 s> HTTP/1.1 200 OK\r\n
312 s> HTTP/1.1 200 OK\r\n
313 s> Server: testing stub value\r\n
313 s> Server: testing stub value\r\n
314 s> Date: $HTTP_DATE$\r\n
314 s> Date: $HTTP_DATE$\r\n
315 s> Content-Type: application/mercurial-exp-framing-0003\r\n
315 s> Content-Type: application/mercurial-exp-framing-0003\r\n
316 s> Transfer-Encoding: chunked\r\n
316 s> Transfer-Encoding: chunked\r\n
317 s> \r\n
317 s> \r\n
318 s> 25\r\n
318 s> 25\r\n
319 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
319 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
320 s> \r\n
320 s> \r\n
321 s> 0\r\n
321 s> 0\r\n
322 s> \r\n
322 s> \r\n
323
323
324 Authorized request for unknown command is rejected
324 Authorized request for unknown command is rejected
325
325
326 $ sendhttpraw << EOF
326 $ sendhttpraw << EOF
327 > httprequest POST api/$HTTPV2/rw/badcommand
327 > httprequest POST api/$HTTPV2/rw/badcommand
328 > user-agent: test
328 > user-agent: test
329 > accept: $MEDIATYPE
329 > accept: $MEDIATYPE
330 > EOF
330 > EOF
331 using raw connection to peer
331 using raw connection to peer
332 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
332 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
333 s> Accept-Encoding: identity\r\n
333 s> Accept-Encoding: identity\r\n
334 s> accept: application/mercurial-exp-framing-0003\r\n
334 s> accept: application/mercurial-exp-framing-0003\r\n
335 s> user-agent: test\r\n
335 s> user-agent: test\r\n
336 s> host: $LOCALIP:$HGPORT\r\n (glob)
336 s> host: $LOCALIP:$HGPORT\r\n (glob)
337 s> \r\n
337 s> \r\n
338 s> makefile('rb', None)
338 s> makefile('rb', None)
339 s> HTTP/1.1 404 Not Found\r\n
339 s> HTTP/1.1 404 Not Found\r\n
340 s> Server: testing stub value\r\n
340 s> Server: testing stub value\r\n
341 s> Date: $HTTP_DATE$\r\n
341 s> Date: $HTTP_DATE$\r\n
342 s> Content-Type: text/plain\r\n
342 s> Content-Type: text/plain\r\n
343 s> Content-Length: 42\r\n
343 s> Content-Length: 42\r\n
344 s> \r\n
344 s> \r\n
345 s> unknown wire protocol command: badcommand\n
345 s> unknown wire protocol command: badcommand\n
346
346
347 debugreflect isn't enabled by default
347 debugreflect isn't enabled by default
348
348
349 $ sendhttpraw << EOF
349 $ sendhttpraw << EOF
350 > httprequest POST api/$HTTPV2/ro/debugreflect
350 > httprequest POST api/$HTTPV2/ro/debugreflect
351 > user-agent: test
351 > user-agent: test
352 > EOF
352 > EOF
353 using raw connection to peer
353 using raw connection to peer
354 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
354 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
355 s> Accept-Encoding: identity\r\n
355 s> Accept-Encoding: identity\r\n
356 s> user-agent: test\r\n
356 s> user-agent: test\r\n
357 s> host: $LOCALIP:$HGPORT\r\n (glob)
357 s> host: $LOCALIP:$HGPORT\r\n (glob)
358 s> \r\n
358 s> \r\n
359 s> makefile('rb', None)
359 s> makefile('rb', None)
360 s> HTTP/1.1 404 Not Found\r\n
360 s> HTTP/1.1 404 Not Found\r\n
361 s> Server: testing stub value\r\n
361 s> Server: testing stub value\r\n
362 s> Date: $HTTP_DATE$\r\n
362 s> Date: $HTTP_DATE$\r\n
363 s> Content-Type: text/plain\r\n
363 s> Content-Type: text/plain\r\n
364 s> Content-Length: 34\r\n
364 s> Content-Length: 34\r\n
365 s> \r\n
365 s> \r\n
366 s> debugreflect service not available
366 s> debugreflect service not available
367
367
368 Restart server to get debugreflect endpoint
368 Restart server to get debugreflect endpoint
369
369
370 $ killdaemons.py
370 $ killdaemons.py
371 $ cat > server/.hg/hgrc << EOF
371 $ cat > server/.hg/hgrc << EOF
372 > [experimental]
372 > [experimental]
373 > web.apiserver = true
373 > web.apiserver = true
374 > web.api.debugreflect = true
374 > web.api.debugreflect = true
375 > web.api.http-v2 = true
375 > web.api.http-v2 = true
376 > [web]
376 > [web]
377 > push_ssl = false
377 > push_ssl = false
378 > allow-push = *
378 > allow-push = *
379 > EOF
379 > EOF
380
380
381 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
381 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
382 $ cat hg.pid > $DAEMON_PIDS
382 $ cat hg.pid > $DAEMON_PIDS
383
383
384 Command frames can be reflected via debugreflect
384 Command frames can be reflected via debugreflect
385
385
386 $ sendhttpraw << EOF
386 $ sendhttpraw << EOF
387 > httprequest POST api/$HTTPV2/ro/debugreflect
387 > httprequest POST api/$HTTPV2/ro/debugreflect
388 > accept: $MEDIATYPE
388 > accept: $MEDIATYPE
389 > content-type: $MEDIATYPE
389 > content-type: $MEDIATYPE
390 > user-agent: test
390 > user-agent: test
391 > frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
391 > frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
392 > EOF
392 > EOF
393 using raw connection to peer
393 using raw connection to peer
394 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
394 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
395 s> Accept-Encoding: identity\r\n
395 s> Accept-Encoding: identity\r\n
396 s> accept: application/mercurial-exp-framing-0003\r\n
396 s> accept: application/mercurial-exp-framing-0003\r\n
397 s> content-type: application/mercurial-exp-framing-0003\r\n
397 s> content-type: application/mercurial-exp-framing-0003\r\n
398 s> user-agent: test\r\n
398 s> user-agent: test\r\n
399 s> content-length: 47\r\n
399 s> content-length: 47\r\n
400 s> host: $LOCALIP:$HGPORT\r\n (glob)
400 s> host: $LOCALIP:$HGPORT\r\n (glob)
401 s> \r\n
401 s> \r\n
402 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1
402 s> \'\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1
403 s> makefile('rb', None)
403 s> makefile('rb', None)
404 s> HTTP/1.1 200 OK\r\n
404 s> HTTP/1.1 200 OK\r\n
405 s> Server: testing stub value\r\n
405 s> Server: testing stub value\r\n
406 s> Date: $HTTP_DATE$\r\n
406 s> Date: $HTTP_DATE$\r\n
407 s> Content-Type: text/plain\r\n
407 s> Content-Type: text/plain\r\n
408 s> Content-Length: 205\r\n
408 s> Content-Length: 205\r\n
409 s> \r\n
409 s> \r\n
410 s> received: 1 1 1 \xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1\n
410 s> received: 1 1 1 \xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1\n
411 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
411 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
412 s> received: <no frame>\n
412 s> received: <no frame>\n
413 s> {"action": "noop"}
413 s> {"action": "noop"}
414
414
415 Multiple requests to regular command URL are not allowed
415 Multiple requests to regular command URL are not allowed
416
416
417 $ sendhttpraw << EOF
417 $ sendhttpraw << EOF
418 > httprequest POST api/$HTTPV2/ro/customreadonly
418 > httprequest POST api/$HTTPV2/ro/customreadonly
419 > accept: $MEDIATYPE
419 > accept: $MEDIATYPE
420 > content-type: $MEDIATYPE
420 > content-type: $MEDIATYPE
421 > user-agent: test
421 > user-agent: test
422 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
422 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
423 > EOF
423 > EOF
424 using raw connection to peer
424 using raw connection to peer
425 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
425 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
426 s> Accept-Encoding: identity\r\n
426 s> Accept-Encoding: identity\r\n
427 s> accept: application/mercurial-exp-framing-0003\r\n
427 s> accept: application/mercurial-exp-framing-0003\r\n
428 s> content-type: application/mercurial-exp-framing-0003\r\n
428 s> content-type: application/mercurial-exp-framing-0003\r\n
429 s> user-agent: test\r\n
429 s> user-agent: test\r\n
430 s> content-length: 29\r\n
430 s> content-length: 29\r\n
431 s> host: $LOCALIP:$HGPORT\r\n (glob)
431 s> host: $LOCALIP:$HGPORT\r\n (glob)
432 s> \r\n
432 s> \r\n
433 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
433 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
434 s> makefile('rb', None)
434 s> makefile('rb', None)
435 s> HTTP/1.1 200 OK\r\n
435 s> HTTP/1.1 200 OK\r\n
436 s> Server: testing stub value\r\n
436 s> Server: testing stub value\r\n
437 s> Date: $HTTP_DATE$\r\n
437 s> Date: $HTTP_DATE$\r\n
438 s> Content-Type: application/mercurial-exp-framing-0003\r\n
438 s> Content-Type: application/mercurial-exp-framing-0003\r\n
439 s> Transfer-Encoding: chunked\r\n
439 s> Transfer-Encoding: chunked\r\n
440 s> \r\n
440 s> \r\n
441 s> 25\r\n
441 s> 25\r\n
442 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
442 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
443 s> \r\n
443 s> \r\n
444 s> 0\r\n
444 s> 0\r\n
445 s> \r\n
445 s> \r\n
446
446
447 Multiple requests to "multirequest" URL are allowed
447 Multiple requests to "multirequest" URL are allowed
448
448
449 $ sendhttpraw << EOF
449 $ sendhttpraw << EOF
450 > httprequest POST api/$HTTPV2/ro/multirequest
450 > httprequest POST api/$HTTPV2/ro/multirequest
451 > accept: $MEDIATYPE
451 > accept: $MEDIATYPE
452 > content-type: $MEDIATYPE
452 > content-type: $MEDIATYPE
453 > user-agent: test
453 > user-agent: test
454 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
454 > frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
455 > frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
455 > frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
456 > EOF
456 > EOF
457 using raw connection to peer
457 using raw connection to peer
458 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
458 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
459 s> Accept-Encoding: identity\r\n
459 s> Accept-Encoding: identity\r\n
460 s> *\r\n (glob)
460 s> *\r\n (glob)
461 s> *\r\n (glob)
461 s> *\r\n (glob)
462 s> user-agent: test\r\n
462 s> user-agent: test\r\n
463 s> content-length: 58\r\n
463 s> content-length: 58\r\n
464 s> host: $LOCALIP:$HGPORT\r\n (glob)
464 s> host: $LOCALIP:$HGPORT\r\n (glob)
465 s> \r\n
465 s> \r\n
466 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
466 s> \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
467 s> makefile('rb', None)
467 s> makefile('rb', None)
468 s> HTTP/1.1 200 OK\r\n
468 s> HTTP/1.1 200 OK\r\n
469 s> Server: testing stub value\r\n
469 s> Server: testing stub value\r\n
470 s> Date: $HTTP_DATE$\r\n
470 s> Date: $HTTP_DATE$\r\n
471 s> Content-Type: application/mercurial-exp-framing-0003\r\n
471 s> Content-Type: application/mercurial-exp-framing-0003\r\n
472 s> Transfer-Encoding: chunked\r\n
472 s> Transfer-Encoding: chunked\r\n
473 s> \r\n
473 s> \r\n
474 s> 25\r\n
474 s> 25\r\n
475 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
475 s> \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
476 s> \r\n
476 s> \r\n
477 s> 25\r\n
477 s> 25\r\n
478 s> \x1d\x00\x00\x03\x00\x02\x00Bcustomreadonly bytes response
478 s> \x1d\x00\x00\x03\x00\x02\x00Bcustomreadonly bytes response
479 s> \r\n
479 s> \r\n
480 s> 0\r\n
480 s> 0\r\n
481 s> \r\n
481 s> \r\n
482
482
483 Interleaved requests to "multirequest" are processed
483 Interleaved requests to "multirequest" are processed
484
484
485 $ sendhttpraw << EOF
485 $ sendhttpraw << EOF
486 > httprequest POST api/$HTTPV2/ro/multirequest
486 > httprequest POST api/$HTTPV2/ro/multirequest
487 > accept: $MEDIATYPE
487 > accept: $MEDIATYPE
488 > content-type: $MEDIATYPE
488 > content-type: $MEDIATYPE
489 > user-agent: test
489 > user-agent: test
490 > frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
490 > frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
491 > frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
491 > frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
492 > frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
492 > frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
493 > frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
493 > frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
494 > EOF
494 > EOF
495 using raw connection to peer
495 using raw connection to peer
496 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
496 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
497 s> Accept-Encoding: identity\r\n
497 s> Accept-Encoding: identity\r\n
498 s> accept: application/mercurial-exp-framing-0003\r\n
498 s> accept: application/mercurial-exp-framing-0003\r\n
499 s> content-type: application/mercurial-exp-framing-0003\r\n
499 s> content-type: application/mercurial-exp-framing-0003\r\n
500 s> user-agent: test\r\n
500 s> user-agent: test\r\n
501 s> content-length: 115\r\n
501 s> content-length: 115\r\n
502 s> host: $LOCALIP:$HGPORT\r\n (glob)
502 s> host: $LOCALIP:$HGPORT\r\n (glob)
503 s> \r\n
503 s> \r\n
504 s> \x11\x00\x00\x01\x00\x01\x01\x15\xa2Dargs\xa1Inamespace\x11\x00\x00\x03\x00\x01\x00\x15\xa2Dargs\xa1Inamespace\x19\x00\x00\x03\x00\x01\x00\x12JnamespacesDnameHlistkeys\x18\x00\x00\x01\x00\x01\x00\x12IbookmarksDnameHlistkeys
504 s> \x11\x00\x00\x01\x00\x01\x01\x15\xa2Dargs\xa1Inamespace\x11\x00\x00\x03\x00\x01\x00\x15\xa2Dargs\xa1Inamespace\x19\x00\x00\x03\x00\x01\x00\x12JnamespacesDnameHlistkeys\x18\x00\x00\x01\x00\x01\x00\x12IbookmarksDnameHlistkeys
505 s> makefile('rb', None)
505 s> makefile('rb', None)
506 s> HTTP/1.1 200 OK\r\n
506 s> HTTP/1.1 200 OK\r\n
507 s> Server: testing stub value\r\n
507 s> Server: testing stub value\r\n
508 s> Date: $HTTP_DATE$\r\n
508 s> Date: $HTTP_DATE$\r\n
509 s> Content-Type: application/mercurial-exp-framing-0003\r\n
509 s> Content-Type: application/mercurial-exp-framing-0003\r\n
510 s> Transfer-Encoding: chunked\r\n
510 s> Transfer-Encoding: chunked\r\n
511 s> \r\n
511 s> \r\n
512 s> 28\r\n
512 s> 28\r\n
513 s> \x00\x00\x03\x00\x02\x01F\xa3Fphases@Ibookmarks@Jnamespaces@
513 s> \x00\x00\x03\x00\x02\x01F\xa3Fphases@Ibookmarks@Jnamespaces@
514 s> \r\n
514 s> \r\n
515 s> 9\r\n
515 s> 9\r\n
516 s> \x01\x00\x00\x01\x00\x02\x00F\xa0
516 s> \x01\x00\x00\x01\x00\x02\x00F\xa0
517 s> \r\n
517 s> \r\n
518 s> 0\r\n
518 s> 0\r\n
519 s> \r\n
519 s> \r\n
520
520
521 Restart server to disable read-write access
521 Restart server to disable read-write access
522
522
523 $ killdaemons.py
523 $ killdaemons.py
524 $ cat > server/.hg/hgrc << EOF
524 $ cat > server/.hg/hgrc << EOF
525 > [experimental]
525 > [experimental]
526 > web.apiserver = true
526 > web.apiserver = true
527 > web.api.debugreflect = true
527 > web.api.debugreflect = true
528 > web.api.http-v2 = true
528 > web.api.http-v2 = true
529 > [web]
529 > [web]
530 > push_ssl = false
530 > push_ssl = false
531 > EOF
531 > EOF
532
532
533 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
533 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
534 $ cat hg.pid > $DAEMON_PIDS
534 $ cat hg.pid > $DAEMON_PIDS
535
535
536 Attempting to run a read-write command via multirequest on read-only URL is not allowed
536 Attempting to run a read-write command via multirequest on read-only URL is not allowed
537
537
538 $ sendhttpraw << EOF
538 $ sendhttpraw << EOF
539 > httprequest POST api/$HTTPV2/ro/multirequest
539 > httprequest POST api/$HTTPV2/ro/multirequest
540 > accept: $MEDIATYPE
540 > accept: $MEDIATYPE
541 > content-type: $MEDIATYPE
541 > content-type: $MEDIATYPE
542 > user-agent: test
542 > user-agent: test
543 > frame 1 1 stream-begin command-request new cbor:{b'name': b'unbundle'}
543 > frame 1 1 stream-begin command-request new cbor:{b'name': b'pushkey'}
544 > EOF
544 > EOF
545 using raw connection to peer
545 using raw connection to peer
546 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
546 s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
547 s> Accept-Encoding: identity\r\n
547 s> Accept-Encoding: identity\r\n
548 s> accept: application/mercurial-exp-framing-0003\r\n
548 s> accept: application/mercurial-exp-framing-0003\r\n
549 s> content-type: application/mercurial-exp-framing-0003\r\n
549 s> content-type: application/mercurial-exp-framing-0003\r\n
550 s> user-agent: test\r\n
550 s> user-agent: test\r\n
551 s> content-length: 23\r\n
551 s> content-length: 22\r\n
552 s> host: $LOCALIP:$HGPORT\r\n (glob)
552 s> host: $LOCALIP:$HGPORT\r\n (glob)
553 s> \r\n
553 s> \r\n
554 s> \x0f\x00\x00\x01\x00\x01\x01\x11\xa1DnameHunbundle
554 s> \x0e\x00\x00\x01\x00\x01\x01\x11\xa1DnameGpushkey
555 s> makefile('rb', None)
555 s> makefile('rb', None)
556 s> HTTP/1.1 403 Forbidden\r\n
556 s> HTTP/1.1 403 Forbidden\r\n
557 s> Server: testing stub value\r\n
557 s> Server: testing stub value\r\n
558 s> Date: $HTTP_DATE$\r\n
558 s> Date: $HTTP_DATE$\r\n
559 s> Content-Type: text/plain\r\n
559 s> Content-Type: text/plain\r\n
560 s> Content-Length: 53\r\n
560 s> Content-Length: 52\r\n
561 s> \r\n
561 s> \r\n
562 s> insufficient permissions to execute command: unbundle
562 s> insufficient permissions to execute command: pushkey
563
563
564 $ cat error.log
564 $ cat error.log
@@ -1,40 +1,40 b''
1 $ . $TESTDIR/wireprotohelpers.sh
1 $ . $TESTDIR/wireprotohelpers.sh
2
2
3 $ hg init server
3 $ hg init server
4 $ enablehttpv2 server
4 $ enablehttpv2 server
5 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
5 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
6 $ cat hg.pid > $DAEMON_PIDS
6 $ cat hg.pid > $DAEMON_PIDS
7
7
8 capabilities request returns an array of capability strings
8 capabilities request returns an array of capability strings
9
9
10 $ sendhttpv2peer << EOF
10 $ sendhttpv2peer << EOF
11 > command capabilities
11 > command capabilities
12 > EOF
12 > EOF
13 creating http peer for wire protocol version 2
13 creating http peer for wire protocol version 2
14 sending capabilities command
14 sending capabilities command
15 s> POST /api/exp-http-v2-0001/ro/capabilities HTTP/1.1\r\n
15 s> POST /api/exp-http-v2-0001/ro/capabilities HTTP/1.1\r\n
16 s> Accept-Encoding: identity\r\n
16 s> Accept-Encoding: identity\r\n
17 s> accept: application/mercurial-exp-framing-0003\r\n
17 s> accept: application/mercurial-exp-framing-0003\r\n
18 s> content-type: application/mercurial-exp-framing-0003\r\n
18 s> content-type: application/mercurial-exp-framing-0003\r\n
19 s> content-length: 27\r\n
19 s> content-length: 27\r\n
20 s> host: $LOCALIP:$HGPORT\r\n (glob)
20 s> host: $LOCALIP:$HGPORT\r\n (glob)
21 s> user-agent: Mercurial debugwireproto\r\n
21 s> user-agent: Mercurial debugwireproto\r\n
22 s> \r\n
22 s> \r\n
23 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
23 s> \x13\x00\x00\x01\x00\x01\x01\x11\xa1DnameLcapabilities
24 s> makefile('rb', None)
24 s> makefile('rb', None)
25 s> HTTP/1.1 200 OK\r\n
25 s> HTTP/1.1 200 OK\r\n
26 s> Server: testing stub value\r\n
26 s> Server: testing stub value\r\n
27 s> Date: $HTTP_DATE$\r\n
27 s> Date: $HTTP_DATE$\r\n
28 s> Content-Type: application/mercurial-exp-framing-0003\r\n
28 s> Content-Type: application/mercurial-exp-framing-0003\r\n
29 s> Transfer-Encoding: chunked\r\n
29 s> Transfer-Encoding: chunked\r\n
30 s> \r\n
30 s> \r\n
31 s> *\r\n (glob)
31 s> *\r\n (glob)
32 s> *\x00\x01\x00\x02\x01F (glob)
32 s> *\x00\x01\x00\x02\x01F (glob)
33 s> \xa2Hcommands\xa9Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullHunbundle\xa2Dargs\xa1EheadsFlegacyKpermissions\x81DpushIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullIgetbundle\xa2Dargs\xa1A*FlegacyKpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlib
33 s> \xa2Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlib
34 s> \r\n
34 s> \r\n
35 received frame(size=*; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) (glob)
35 received frame(size=*; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) (glob)
36 s> 0\r\n
36 s> 0\r\n
37 s> \r\n
37 s> \r\n
38 response: [{b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'getbundle': {b'args': {b'*': b'legacy'}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}, b'unbundle': {b'args': {b'heads': b'legacy'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}]}]
38 response: [{b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}]}]
39
39
40 $ cat error.log
40 $ cat error.log
General Comments 0
You need to be logged in to leave comments. Login now