##// END OF EJS Templates
repository: port peer interfaces to zope.interface...
Gregory Szorc -
r37336:39f7d4ee default
parent child Browse files
Show More
@@ -1,506 +1,506 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import tempfile
16 import tempfile
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 error,
21 error,
22 httpconnection,
22 httpconnection,
23 pycompat,
23 pycompat,
24 statichttprepo,
24 statichttprepo,
25 url as urlmod,
25 url as urlmod,
26 util,
26 util,
27 wireproto,
27 wireproto,
28 )
28 )
29
29
30 httplib = util.httplib
30 httplib = util.httplib
31 urlerr = util.urlerr
31 urlerr = util.urlerr
32 urlreq = util.urlreq
32 urlreq = util.urlreq
33
33
34 def encodevalueinheaders(value, header, limit):
34 def encodevalueinheaders(value, header, limit):
35 """Encode a string value into multiple HTTP headers.
35 """Encode a string value into multiple HTTP headers.
36
36
37 ``value`` will be encoded into 1 or more HTTP headers with the names
37 ``value`` will be encoded into 1 or more HTTP headers with the names
38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 name + value will be at most ``limit`` bytes long.
39 name + value will be at most ``limit`` bytes long.
40
40
41 Returns an iterable of 2-tuples consisting of header names and
41 Returns an iterable of 2-tuples consisting of header names and
42 values as native strings.
42 values as native strings.
43 """
43 """
44 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
44 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
45 # not bytes. This function always takes bytes in as arguments.
45 # not bytes. This function always takes bytes in as arguments.
46 fmt = pycompat.strurl(header) + r'-%s'
46 fmt = pycompat.strurl(header) + r'-%s'
47 # Note: it is *NOT* a bug that the last bit here is a bytestring
47 # Note: it is *NOT* a bug that the last bit here is a bytestring
48 # and not a unicode: we're just getting the encoded length anyway,
48 # and not a unicode: we're just getting the encoded length anyway,
49 # and using an r-string to make it portable between Python 2 and 3
49 # and using an r-string to make it portable between Python 2 and 3
50 # doesn't work because then the \r is a literal backslash-r
50 # doesn't work because then the \r is a literal backslash-r
51 # instead of a carriage return.
51 # instead of a carriage return.
52 valuelen = limit - len(fmt % r'000') - len(': \r\n')
52 valuelen = limit - len(fmt % r'000') - len(': \r\n')
53 result = []
53 result = []
54
54
55 n = 0
55 n = 0
56 for i in xrange(0, len(value), valuelen):
56 for i in xrange(0, len(value), valuelen):
57 n += 1
57 n += 1
58 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
58 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
59
59
60 return result
60 return result
61
61
62 def _wraphttpresponse(resp):
62 def _wraphttpresponse(resp):
63 """Wrap an HTTPResponse with common error handlers.
63 """Wrap an HTTPResponse with common error handlers.
64
64
65 This ensures that any I/O from any consumer raises the appropriate
65 This ensures that any I/O from any consumer raises the appropriate
66 error and messaging.
66 error and messaging.
67 """
67 """
68 origread = resp.read
68 origread = resp.read
69
69
70 class readerproxy(resp.__class__):
70 class readerproxy(resp.__class__):
71 def read(self, size=None):
71 def read(self, size=None):
72 try:
72 try:
73 return origread(size)
73 return origread(size)
74 except httplib.IncompleteRead as e:
74 except httplib.IncompleteRead as e:
75 # e.expected is an integer if length known or None otherwise.
75 # e.expected is an integer if length known or None otherwise.
76 if e.expected:
76 if e.expected:
77 msg = _('HTTP request error (incomplete response; '
77 msg = _('HTTP request error (incomplete response; '
78 'expected %d bytes got %d)') % (e.expected,
78 'expected %d bytes got %d)') % (e.expected,
79 len(e.partial))
79 len(e.partial))
80 else:
80 else:
81 msg = _('HTTP request error (incomplete response)')
81 msg = _('HTTP request error (incomplete response)')
82
82
83 raise error.PeerTransportError(
83 raise error.PeerTransportError(
84 msg,
84 msg,
85 hint=_('this may be an intermittent network failure; '
85 hint=_('this may be an intermittent network failure; '
86 'if the error persists, consider contacting the '
86 'if the error persists, consider contacting the '
87 'network or server operator'))
87 'network or server operator'))
88 except httplib.HTTPException as e:
88 except httplib.HTTPException as e:
89 raise error.PeerTransportError(
89 raise error.PeerTransportError(
90 _('HTTP request error (%s)') % e,
90 _('HTTP request error (%s)') % e,
91 hint=_('this may be an intermittent network failure; '
91 hint=_('this may be an intermittent network failure; '
92 'if the error persists, consider contacting the '
92 'if the error persists, consider contacting the '
93 'network or server operator'))
93 'network or server operator'))
94
94
95 resp.__class__ = readerproxy
95 resp.__class__ = readerproxy
96
96
97 class _multifile(object):
97 class _multifile(object):
98 def __init__(self, *fileobjs):
98 def __init__(self, *fileobjs):
99 for f in fileobjs:
99 for f in fileobjs:
100 if not util.safehasattr(f, 'length'):
100 if not util.safehasattr(f, 'length'):
101 raise ValueError(
101 raise ValueError(
102 '_multifile only supports file objects that '
102 '_multifile only supports file objects that '
103 'have a length but this one does not:', type(f), f)
103 'have a length but this one does not:', type(f), f)
104 self._fileobjs = fileobjs
104 self._fileobjs = fileobjs
105 self._index = 0
105 self._index = 0
106
106
107 @property
107 @property
108 def length(self):
108 def length(self):
109 return sum(f.length for f in self._fileobjs)
109 return sum(f.length for f in self._fileobjs)
110
110
111 def read(self, amt=None):
111 def read(self, amt=None):
112 if amt <= 0:
112 if amt <= 0:
113 return ''.join(f.read() for f in self._fileobjs)
113 return ''.join(f.read() for f in self._fileobjs)
114 parts = []
114 parts = []
115 while amt and self._index < len(self._fileobjs):
115 while amt and self._index < len(self._fileobjs):
116 parts.append(self._fileobjs[self._index].read(amt))
116 parts.append(self._fileobjs[self._index].read(amt))
117 got = len(parts[-1])
117 got = len(parts[-1])
118 if got < amt:
118 if got < amt:
119 self._index += 1
119 self._index += 1
120 amt -= got
120 amt -= got
121 return ''.join(parts)
121 return ''.join(parts)
122
122
123 def seek(self, offset, whence=os.SEEK_SET):
123 def seek(self, offset, whence=os.SEEK_SET):
124 if whence != os.SEEK_SET:
124 if whence != os.SEEK_SET:
125 raise NotImplementedError(
125 raise NotImplementedError(
126 '_multifile does not support anything other'
126 '_multifile does not support anything other'
127 ' than os.SEEK_SET for whence on seek()')
127 ' than os.SEEK_SET for whence on seek()')
128 if offset != 0:
128 if offset != 0:
129 raise NotImplementedError(
129 raise NotImplementedError(
130 '_multifile only supports seeking to start, but that '
130 '_multifile only supports seeking to start, but that '
131 'could be fixed if you need it')
131 'could be fixed if you need it')
132 for f in self._fileobjs:
132 for f in self._fileobjs:
133 f.seek(0)
133 f.seek(0)
134 self._index = 0
134 self._index = 0
135
135
136 class httppeer(wireproto.wirepeer):
136 class httppeer(wireproto.wirepeer):
137 def __init__(self, ui, path, url, opener):
137 def __init__(self, ui, path, url, opener):
138 self._ui = ui
138 self._ui = ui
139 self._path = path
139 self._path = path
140 self._url = url
140 self._url = url
141 self._caps = None
141 self._caps = None
142 self._urlopener = opener
142 self._urlopener = opener
143 # This is an its own attribute to facilitate extensions overriding
143 # This is an its own attribute to facilitate extensions overriding
144 # the default type.
144 # the default type.
145 self._requestbuilder = urlreq.request
145 self._requestbuilder = urlreq.request
146
146
147 def __del__(self):
147 def __del__(self):
148 for h in self._urlopener.handlers:
148 for h in self._urlopener.handlers:
149 h.close()
149 h.close()
150 getattr(h, "close_all", lambda: None)()
150 getattr(h, "close_all", lambda: None)()
151
151
152 def _openurl(self, req):
152 def _openurl(self, req):
153 if (self._ui.debugflag
153 if (self._ui.debugflag
154 and self._ui.configbool('devel', 'debug.peer-request')):
154 and self._ui.configbool('devel', 'debug.peer-request')):
155 dbg = self._ui.debug
155 dbg = self._ui.debug
156 line = 'devel-peer-request: %s\n'
156 line = 'devel-peer-request: %s\n'
157 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
157 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
158 hgargssize = None
158 hgargssize = None
159
159
160 for header, value in sorted(req.header_items()):
160 for header, value in sorted(req.header_items()):
161 if header.startswith('X-hgarg-'):
161 if header.startswith('X-hgarg-'):
162 if hgargssize is None:
162 if hgargssize is None:
163 hgargssize = 0
163 hgargssize = 0
164 hgargssize += len(value)
164 hgargssize += len(value)
165 else:
165 else:
166 dbg(line % ' %s %s' % (header, value))
166 dbg(line % ' %s %s' % (header, value))
167
167
168 if hgargssize is not None:
168 if hgargssize is not None:
169 dbg(line % ' %d bytes of commands arguments in headers'
169 dbg(line % ' %d bytes of commands arguments in headers'
170 % hgargssize)
170 % hgargssize)
171
171
172 if req.has_data():
172 if req.has_data():
173 data = req.get_data()
173 data = req.get_data()
174 length = getattr(data, 'length', None)
174 length = getattr(data, 'length', None)
175 if length is None:
175 if length is None:
176 length = len(data)
176 length = len(data)
177 dbg(line % ' %d bytes of data' % length)
177 dbg(line % ' %d bytes of data' % length)
178
178
179 start = util.timer()
179 start = util.timer()
180
180
181 ret = self._urlopener.open(req)
181 ret = self._urlopener.open(req)
182 if self._ui.configbool('devel', 'debug.peer-request'):
182 if self._ui.configbool('devel', 'debug.peer-request'):
183 dbg(line % ' finished in %.4f seconds (%s)'
183 dbg(line % ' finished in %.4f seconds (%s)'
184 % (util.timer() - start, ret.code))
184 % (util.timer() - start, ret.code))
185 return ret
185 return ret
186
186
187 # Begin of _basepeer interface.
187 # Begin of ipeerconnection interface.
188
188
189 @util.propertycache
189 @util.propertycache
190 def ui(self):
190 def ui(self):
191 return self._ui
191 return self._ui
192
192
193 def url(self):
193 def url(self):
194 return self._path
194 return self._path
195
195
196 def local(self):
196 def local(self):
197 return None
197 return None
198
198
199 def peer(self):
199 def peer(self):
200 return self
200 return self
201
201
202 def canpush(self):
202 def canpush(self):
203 return True
203 return True
204
204
205 def close(self):
205 def close(self):
206 pass
206 pass
207
207
208 # End of _basepeer interface.
208 # End of ipeerconnection interface.
209
209
210 # Begin of _basewirepeer interface.
210 # Begin of ipeercommands interface.
211
211
212 def capabilities(self):
212 def capabilities(self):
213 # self._fetchcaps() should have been called as part of peer
213 # self._fetchcaps() should have been called as part of peer
214 # handshake. So self._caps should always be set.
214 # handshake. So self._caps should always be set.
215 assert self._caps is not None
215 assert self._caps is not None
216 return self._caps
216 return self._caps
217
217
218 # End of _basewirepeer interface.
218 # End of ipeercommands interface.
219
219
220 # look up capabilities only when needed
220 # look up capabilities only when needed
221
221
222 def _fetchcaps(self):
222 def _fetchcaps(self):
223 self._caps = set(self._call('capabilities').split())
223 self._caps = set(self._call('capabilities').split())
224
224
225 def _callstream(self, cmd, _compressible=False, **args):
225 def _callstream(self, cmd, _compressible=False, **args):
226 args = pycompat.byteskwargs(args)
226 args = pycompat.byteskwargs(args)
227 if cmd == 'pushkey':
227 if cmd == 'pushkey':
228 args['data'] = ''
228 args['data'] = ''
229 data = args.pop('data', None)
229 data = args.pop('data', None)
230 headers = args.pop('headers', {})
230 headers = args.pop('headers', {})
231
231
232 self.ui.debug("sending %s command\n" % cmd)
232 self.ui.debug("sending %s command\n" % cmd)
233 q = [('cmd', cmd)]
233 q = [('cmd', cmd)]
234 headersize = 0
234 headersize = 0
235 varyheaders = []
235 varyheaders = []
236 # Important: don't use self.capable() here or else you end up
236 # Important: don't use self.capable() here or else you end up
237 # with infinite recursion when trying to look up capabilities
237 # with infinite recursion when trying to look up capabilities
238 # for the first time.
238 # for the first time.
239 postargsok = self._caps is not None and 'httppostargs' in self._caps
239 postargsok = self._caps is not None and 'httppostargs' in self._caps
240
240
241 # Send arguments via POST.
241 # Send arguments via POST.
242 if postargsok and args:
242 if postargsok and args:
243 strargs = urlreq.urlencode(sorted(args.items()))
243 strargs = urlreq.urlencode(sorted(args.items()))
244 if not data:
244 if not data:
245 data = strargs
245 data = strargs
246 else:
246 else:
247 if isinstance(data, bytes):
247 if isinstance(data, bytes):
248 i = io.BytesIO(data)
248 i = io.BytesIO(data)
249 i.length = len(data)
249 i.length = len(data)
250 data = i
250 data = i
251 argsio = io.BytesIO(strargs)
251 argsio = io.BytesIO(strargs)
252 argsio.length = len(strargs)
252 argsio.length = len(strargs)
253 data = _multifile(argsio, data)
253 data = _multifile(argsio, data)
254 headers[r'X-HgArgs-Post'] = len(strargs)
254 headers[r'X-HgArgs-Post'] = len(strargs)
255 elif args:
255 elif args:
256 # Calling self.capable() can infinite loop if we are calling
256 # Calling self.capable() can infinite loop if we are calling
257 # "capabilities". But that command should never accept wire
257 # "capabilities". But that command should never accept wire
258 # protocol arguments. So this should never happen.
258 # protocol arguments. So this should never happen.
259 assert cmd != 'capabilities'
259 assert cmd != 'capabilities'
260 httpheader = self.capable('httpheader')
260 httpheader = self.capable('httpheader')
261 if httpheader:
261 if httpheader:
262 headersize = int(httpheader.split(',', 1)[0])
262 headersize = int(httpheader.split(',', 1)[0])
263
263
264 # Send arguments via HTTP headers.
264 # Send arguments via HTTP headers.
265 if headersize > 0:
265 if headersize > 0:
266 # The headers can typically carry more data than the URL.
266 # The headers can typically carry more data than the URL.
267 encargs = urlreq.urlencode(sorted(args.items()))
267 encargs = urlreq.urlencode(sorted(args.items()))
268 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
268 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
269 headersize):
269 headersize):
270 headers[header] = value
270 headers[header] = value
271 varyheaders.append(header)
271 varyheaders.append(header)
272 # Send arguments via query string (Mercurial <1.9).
272 # Send arguments via query string (Mercurial <1.9).
273 else:
273 else:
274 q += sorted(args.items())
274 q += sorted(args.items())
275
275
276 qs = '?%s' % urlreq.urlencode(q)
276 qs = '?%s' % urlreq.urlencode(q)
277 cu = "%s%s" % (self._url, qs)
277 cu = "%s%s" % (self._url, qs)
278 size = 0
278 size = 0
279 if util.safehasattr(data, 'length'):
279 if util.safehasattr(data, 'length'):
280 size = data.length
280 size = data.length
281 elif data is not None:
281 elif data is not None:
282 size = len(data)
282 size = len(data)
283 if data is not None and r'Content-Type' not in headers:
283 if data is not None and r'Content-Type' not in headers:
284 headers[r'Content-Type'] = r'application/mercurial-0.1'
284 headers[r'Content-Type'] = r'application/mercurial-0.1'
285
285
286 # Tell the server we accept application/mercurial-0.2 and multiple
286 # Tell the server we accept application/mercurial-0.2 and multiple
287 # compression formats if the server is capable of emitting those
287 # compression formats if the server is capable of emitting those
288 # payloads.
288 # payloads.
289 protoparams = []
289 protoparams = []
290
290
291 mediatypes = set()
291 mediatypes = set()
292 if self._caps is not None:
292 if self._caps is not None:
293 mt = self.capable('httpmediatype')
293 mt = self.capable('httpmediatype')
294 if mt:
294 if mt:
295 protoparams.append('0.1')
295 protoparams.append('0.1')
296 mediatypes = set(mt.split(','))
296 mediatypes = set(mt.split(','))
297
297
298 if '0.2tx' in mediatypes:
298 if '0.2tx' in mediatypes:
299 protoparams.append('0.2')
299 protoparams.append('0.2')
300
300
301 if '0.2tx' in mediatypes and self.capable('compression'):
301 if '0.2tx' in mediatypes and self.capable('compression'):
302 # We /could/ compare supported compression formats and prune
302 # We /could/ compare supported compression formats and prune
303 # non-mutually supported or error if nothing is mutually supported.
303 # non-mutually supported or error if nothing is mutually supported.
304 # For now, send the full list to the server and have it error.
304 # For now, send the full list to the server and have it error.
305 comps = [e.wireprotosupport().name for e in
305 comps = [e.wireprotosupport().name for e in
306 util.compengines.supportedwireengines(util.CLIENTROLE)]
306 util.compengines.supportedwireengines(util.CLIENTROLE)]
307 protoparams.append('comp=%s' % ','.join(comps))
307 protoparams.append('comp=%s' % ','.join(comps))
308
308
309 if protoparams:
309 if protoparams:
310 protoheaders = encodevalueinheaders(' '.join(protoparams),
310 protoheaders = encodevalueinheaders(' '.join(protoparams),
311 'X-HgProto',
311 'X-HgProto',
312 headersize or 1024)
312 headersize or 1024)
313 for header, value in protoheaders:
313 for header, value in protoheaders:
314 headers[header] = value
314 headers[header] = value
315 varyheaders.append(header)
315 varyheaders.append(header)
316
316
317 if varyheaders:
317 if varyheaders:
318 headers[r'Vary'] = r','.join(varyheaders)
318 headers[r'Vary'] = r','.join(varyheaders)
319
319
320 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
320 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
321
321
322 if data is not None:
322 if data is not None:
323 self.ui.debug("sending %d bytes\n" % size)
323 self.ui.debug("sending %d bytes\n" % size)
324 req.add_unredirected_header(r'Content-Length', r'%d' % size)
324 req.add_unredirected_header(r'Content-Length', r'%d' % size)
325 try:
325 try:
326 resp = self._openurl(req)
326 resp = self._openurl(req)
327 except urlerr.httperror as inst:
327 except urlerr.httperror as inst:
328 if inst.code == 401:
328 if inst.code == 401:
329 raise error.Abort(_('authorization failed'))
329 raise error.Abort(_('authorization failed'))
330 raise
330 raise
331 except httplib.HTTPException as inst:
331 except httplib.HTTPException as inst:
332 self.ui.debug('http error while sending %s command\n' % cmd)
332 self.ui.debug('http error while sending %s command\n' % cmd)
333 self.ui.traceback()
333 self.ui.traceback()
334 raise IOError(None, inst)
334 raise IOError(None, inst)
335
335
336 # Insert error handlers for common I/O failures.
336 # Insert error handlers for common I/O failures.
337 _wraphttpresponse(resp)
337 _wraphttpresponse(resp)
338
338
339 # record the url we got redirected to
339 # record the url we got redirected to
340 resp_url = pycompat.bytesurl(resp.geturl())
340 resp_url = pycompat.bytesurl(resp.geturl())
341 if resp_url.endswith(qs):
341 if resp_url.endswith(qs):
342 resp_url = resp_url[:-len(qs)]
342 resp_url = resp_url[:-len(qs)]
343 if self._url.rstrip('/') != resp_url.rstrip('/'):
343 if self._url.rstrip('/') != resp_url.rstrip('/'):
344 if not self.ui.quiet:
344 if not self.ui.quiet:
345 self.ui.warn(_('real URL is %s\n') % resp_url)
345 self.ui.warn(_('real URL is %s\n') % resp_url)
346 self._url = resp_url
346 self._url = resp_url
347 try:
347 try:
348 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
348 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
349 except AttributeError:
349 except AttributeError:
350 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
350 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
351
351
352 safeurl = util.hidepassword(self._url)
352 safeurl = util.hidepassword(self._url)
353 if proto.startswith('application/hg-error'):
353 if proto.startswith('application/hg-error'):
354 raise error.OutOfBandError(resp.read())
354 raise error.OutOfBandError(resp.read())
355 # accept old "text/plain" and "application/hg-changegroup" for now
355 # accept old "text/plain" and "application/hg-changegroup" for now
356 if not (proto.startswith('application/mercurial-') or
356 if not (proto.startswith('application/mercurial-') or
357 (proto.startswith('text/plain')
357 (proto.startswith('text/plain')
358 and not resp.headers.get('content-length')) or
358 and not resp.headers.get('content-length')) or
359 proto.startswith('application/hg-changegroup')):
359 proto.startswith('application/hg-changegroup')):
360 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
360 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
361 raise error.RepoError(
361 raise error.RepoError(
362 _("'%s' does not appear to be an hg repository:\n"
362 _("'%s' does not appear to be an hg repository:\n"
363 "---%%<--- (%s)\n%s\n---%%<---\n")
363 "---%%<--- (%s)\n%s\n---%%<---\n")
364 % (safeurl, proto or 'no content-type', resp.read(1024)))
364 % (safeurl, proto or 'no content-type', resp.read(1024)))
365
365
366 if proto.startswith('application/mercurial-'):
366 if proto.startswith('application/mercurial-'):
367 try:
367 try:
368 version = proto.split('-', 1)[1]
368 version = proto.split('-', 1)[1]
369 version_info = tuple([int(n) for n in version.split('.')])
369 version_info = tuple([int(n) for n in version.split('.')])
370 except ValueError:
370 except ValueError:
371 raise error.RepoError(_("'%s' sent a broken Content-Type "
371 raise error.RepoError(_("'%s' sent a broken Content-Type "
372 "header (%s)") % (safeurl, proto))
372 "header (%s)") % (safeurl, proto))
373
373
374 # TODO consider switching to a decompression reader that uses
374 # TODO consider switching to a decompression reader that uses
375 # generators.
375 # generators.
376 if version_info == (0, 1):
376 if version_info == (0, 1):
377 if _compressible:
377 if _compressible:
378 return util.compengines['zlib'].decompressorreader(resp)
378 return util.compengines['zlib'].decompressorreader(resp)
379 return resp
379 return resp
380 elif version_info == (0, 2):
380 elif version_info == (0, 2):
381 # application/mercurial-0.2 always identifies the compression
381 # application/mercurial-0.2 always identifies the compression
382 # engine in the payload header.
382 # engine in the payload header.
383 elen = struct.unpack('B', resp.read(1))[0]
383 elen = struct.unpack('B', resp.read(1))[0]
384 ename = resp.read(elen)
384 ename = resp.read(elen)
385 engine = util.compengines.forwiretype(ename)
385 engine = util.compengines.forwiretype(ename)
386 return engine.decompressorreader(resp)
386 return engine.decompressorreader(resp)
387 else:
387 else:
388 raise error.RepoError(_("'%s' uses newer protocol %s") %
388 raise error.RepoError(_("'%s' uses newer protocol %s") %
389 (safeurl, version))
389 (safeurl, version))
390
390
391 if _compressible:
391 if _compressible:
392 return util.compengines['zlib'].decompressorreader(resp)
392 return util.compengines['zlib'].decompressorreader(resp)
393
393
394 return resp
394 return resp
395
395
396 def _call(self, cmd, **args):
396 def _call(self, cmd, **args):
397 fp = self._callstream(cmd, **args)
397 fp = self._callstream(cmd, **args)
398 try:
398 try:
399 return fp.read()
399 return fp.read()
400 finally:
400 finally:
401 # if using keepalive, allow connection to be reused
401 # if using keepalive, allow connection to be reused
402 fp.close()
402 fp.close()
403
403
404 def _callpush(self, cmd, cg, **args):
404 def _callpush(self, cmd, cg, **args):
405 # have to stream bundle to a temp file because we do not have
405 # have to stream bundle to a temp file because we do not have
406 # http 1.1 chunked transfer.
406 # http 1.1 chunked transfer.
407
407
408 types = self.capable('unbundle')
408 types = self.capable('unbundle')
409 try:
409 try:
410 types = types.split(',')
410 types = types.split(',')
411 except AttributeError:
411 except AttributeError:
412 # servers older than d1b16a746db6 will send 'unbundle' as a
412 # servers older than d1b16a746db6 will send 'unbundle' as a
413 # boolean capability. They only support headerless/uncompressed
413 # boolean capability. They only support headerless/uncompressed
414 # bundles.
414 # bundles.
415 types = [""]
415 types = [""]
416 for x in types:
416 for x in types:
417 if x in bundle2.bundletypes:
417 if x in bundle2.bundletypes:
418 type = x
418 type = x
419 break
419 break
420
420
421 tempname = bundle2.writebundle(self.ui, cg, None, type)
421 tempname = bundle2.writebundle(self.ui, cg, None, type)
422 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
422 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
423 headers = {r'Content-Type': r'application/mercurial-0.1'}
423 headers = {r'Content-Type': r'application/mercurial-0.1'}
424
424
425 try:
425 try:
426 r = self._call(cmd, data=fp, headers=headers, **args)
426 r = self._call(cmd, data=fp, headers=headers, **args)
427 vals = r.split('\n', 1)
427 vals = r.split('\n', 1)
428 if len(vals) < 2:
428 if len(vals) < 2:
429 raise error.ResponseError(_("unexpected response:"), r)
429 raise error.ResponseError(_("unexpected response:"), r)
430 return vals
430 return vals
431 except urlerr.httperror:
431 except urlerr.httperror:
432 # Catch and re-raise these so we don't try and treat them
432 # Catch and re-raise these so we don't try and treat them
433 # like generic socket errors. They lack any values in
433 # like generic socket errors. They lack any values in
434 # .args on Python 3 which breaks our socket.error block.
434 # .args on Python 3 which breaks our socket.error block.
435 raise
435 raise
436 except socket.error as err:
436 except socket.error as err:
437 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
437 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
438 raise error.Abort(_('push failed: %s') % err.args[1])
438 raise error.Abort(_('push failed: %s') % err.args[1])
439 raise error.Abort(err.args[1])
439 raise error.Abort(err.args[1])
440 finally:
440 finally:
441 fp.close()
441 fp.close()
442 os.unlink(tempname)
442 os.unlink(tempname)
443
443
444 def _calltwowaystream(self, cmd, fp, **args):
444 def _calltwowaystream(self, cmd, fp, **args):
445 fh = None
445 fh = None
446 fp_ = None
446 fp_ = None
447 filename = None
447 filename = None
448 try:
448 try:
449 # dump bundle to disk
449 # dump bundle to disk
450 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
450 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
451 fh = os.fdopen(fd, r"wb")
451 fh = os.fdopen(fd, r"wb")
452 d = fp.read(4096)
452 d = fp.read(4096)
453 while d:
453 while d:
454 fh.write(d)
454 fh.write(d)
455 d = fp.read(4096)
455 d = fp.read(4096)
456 fh.close()
456 fh.close()
457 # start http push
457 # start http push
458 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
458 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
459 headers = {r'Content-Type': r'application/mercurial-0.1'}
459 headers = {r'Content-Type': r'application/mercurial-0.1'}
460 return self._callstream(cmd, data=fp_, headers=headers, **args)
460 return self._callstream(cmd, data=fp_, headers=headers, **args)
461 finally:
461 finally:
462 if fp_ is not None:
462 if fp_ is not None:
463 fp_.close()
463 fp_.close()
464 if fh is not None:
464 if fh is not None:
465 fh.close()
465 fh.close()
466 os.unlink(filename)
466 os.unlink(filename)
467
467
468 def _callcompressable(self, cmd, **args):
468 def _callcompressable(self, cmd, **args):
469 return self._callstream(cmd, _compressible=True, **args)
469 return self._callstream(cmd, _compressible=True, **args)
470
470
471 def _abort(self, exception):
471 def _abort(self, exception):
472 raise exception
472 raise exception
473
473
474 def makepeer(ui, path):
474 def makepeer(ui, path):
475 u = util.url(path)
475 u = util.url(path)
476 if u.query or u.fragment:
476 if u.query or u.fragment:
477 raise error.Abort(_('unsupported URL component: "%s"') %
477 raise error.Abort(_('unsupported URL component: "%s"') %
478 (u.query or u.fragment))
478 (u.query or u.fragment))
479
479
480 # urllib cannot handle URLs with embedded user or passwd.
480 # urllib cannot handle URLs with embedded user or passwd.
481 url, authinfo = u.authinfo()
481 url, authinfo = u.authinfo()
482 ui.debug('using %s\n' % url)
482 ui.debug('using %s\n' % url)
483
483
484 opener = urlmod.opener(ui, authinfo)
484 opener = urlmod.opener(ui, authinfo)
485
485
486 return httppeer(ui, path, url, opener)
486 return httppeer(ui, path, url, opener)
487
487
488 def instance(ui, path, create):
488 def instance(ui, path, create):
489 if create:
489 if create:
490 raise error.Abort(_('cannot create new http repository'))
490 raise error.Abort(_('cannot create new http repository'))
491 try:
491 try:
492 if path.startswith('https:') and not urlmod.has_https:
492 if path.startswith('https:') and not urlmod.has_https:
493 raise error.Abort(_('Python support for SSL and HTTPS '
493 raise error.Abort(_('Python support for SSL and HTTPS '
494 'is not installed'))
494 'is not installed'))
495
495
496 inst = makepeer(ui, path)
496 inst = makepeer(ui, path)
497 inst._fetchcaps()
497 inst._fetchcaps()
498
498
499 return inst
499 return inst
500 except error.RepoError as httpexception:
500 except error.RepoError as httpexception:
501 try:
501 try:
502 r = statichttprepo.instance(ui, "static-" + path, create)
502 r = statichttprepo.instance(ui, "static-" + path, create)
503 ui.note(_('(falling back to static-http)\n'))
503 ui.note(_('(falling back to static-http)\n'))
504 return r
504 return r
505 except error.RepoError:
505 except error.RepoError:
506 raise httpexception # use the original http RepoError instead
506 raise httpexception # use the original http RepoError instead
@@ -1,622 +1,603 b''
1 # repository.py - Interfaces and base classes for repositories and peers.
1 # repository.py - Interfaces and base classes for repositories and peers.
2 #
2 #
3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.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 abc
11
12 from .i18n import _
10 from .i18n import _
13 from .thirdparty.zope import (
11 from .thirdparty.zope import (
14 interface as zi,
12 interface as zi,
15 )
13 )
16 from . import (
14 from . import (
17 error,
15 error,
18 )
16 )
19
17
20 class _basepeer(object):
18 class ipeerconnection(zi.Interface):
21 """Represents a "connection" to a repository.
19 """Represents a "connection" to a repository.
22
20
23 This is the base interface for representing a connection to a repository.
21 This is the base interface for representing a connection to a repository.
24 It holds basic properties and methods applicable to all peer types.
22 It holds basic properties and methods applicable to all peer types.
25
23
26 This is not a complete interface definition and should not be used
24 This is not a complete interface definition and should not be used
27 outside of this module.
25 outside of this module.
28 """
26 """
29 __metaclass__ = abc.ABCMeta
27 ui = zi.Attribute("""ui.ui instance""")
30
28
31 @abc.abstractproperty
29 def url():
32 def ui(self):
33 """ui.ui instance."""
34
35 @abc.abstractmethod
36 def url(self):
37 """Returns a URL string representing this peer.
30 """Returns a URL string representing this peer.
38
31
39 Currently, implementations expose the raw URL used to construct the
32 Currently, implementations expose the raw URL used to construct the
40 instance. It may contain credentials as part of the URL. The
33 instance. It may contain credentials as part of the URL. The
41 expectations of the value aren't well-defined and this could lead to
34 expectations of the value aren't well-defined and this could lead to
42 data leakage.
35 data leakage.
43
36
44 TODO audit/clean consumers and more clearly define the contents of this
37 TODO audit/clean consumers and more clearly define the contents of this
45 value.
38 value.
46 """
39 """
47
40
48 @abc.abstractmethod
41 def local():
49 def local(self):
50 """Returns a local repository instance.
42 """Returns a local repository instance.
51
43
52 If the peer represents a local repository, returns an object that
44 If the peer represents a local repository, returns an object that
53 can be used to interface with it. Otherwise returns ``None``.
45 can be used to interface with it. Otherwise returns ``None``.
54 """
46 """
55
47
56 @abc.abstractmethod
48 def peer():
57 def peer(self):
58 """Returns an object conforming to this interface.
49 """Returns an object conforming to this interface.
59
50
60 Most implementations will ``return self``.
51 Most implementations will ``return self``.
61 """
52 """
62
53
63 @abc.abstractmethod
54 def canpush():
64 def canpush(self):
65 """Returns a boolean indicating if this peer can be pushed to."""
55 """Returns a boolean indicating if this peer can be pushed to."""
66
56
67 @abc.abstractmethod
57 def close():
68 def close(self):
69 """Close the connection to this peer.
58 """Close the connection to this peer.
70
59
71 This is called when the peer will no longer be used. Resources
60 This is called when the peer will no longer be used. Resources
72 associated with the peer should be cleaned up.
61 associated with the peer should be cleaned up.
73 """
62 """
74
63
75 class _basewirecommands(object):
64 class ipeercommands(zi.Interface):
76 """Client-side interface for communicating over the wire protocol.
65 """Client-side interface for communicating over the wire protocol.
77
66
78 This interface is used as a gateway to the Mercurial wire protocol.
67 This interface is used as a gateway to the Mercurial wire protocol.
79 methods commonly call wire protocol commands of the same name.
68 methods commonly call wire protocol commands of the same name.
80 """
69 """
81 __metaclass__ = abc.ABCMeta
82
70
83 @abc.abstractmethod
71 def branchmap():
84 def branchmap(self):
85 """Obtain heads in named branches.
72 """Obtain heads in named branches.
86
73
87 Returns a dict mapping branch name to an iterable of nodes that are
74 Returns a dict mapping branch name to an iterable of nodes that are
88 heads on that branch.
75 heads on that branch.
89 """
76 """
90
77
91 @abc.abstractmethod
78 def capabilities():
92 def capabilities(self):
93 """Obtain capabilities of the peer.
79 """Obtain capabilities of the peer.
94
80
95 Returns a set of string capabilities.
81 Returns a set of string capabilities.
96 """
82 """
97
83
98 @abc.abstractmethod
84 def debugwireargs(one, two, three=None, four=None, five=None):
99 def debugwireargs(self, one, two, three=None, four=None, five=None):
100 """Used to facilitate debugging of arguments passed over the wire."""
85 """Used to facilitate debugging of arguments passed over the wire."""
101
86
102 @abc.abstractmethod
87 def getbundle(source, **kwargs):
103 def getbundle(self, source, **kwargs):
104 """Obtain remote repository data as a bundle.
88 """Obtain remote repository data as a bundle.
105
89
106 This command is how the bulk of repository data is transferred from
90 This command is how the bulk of repository data is transferred from
107 the peer to the local repository
91 the peer to the local repository
108
92
109 Returns a generator of bundle data.
93 Returns a generator of bundle data.
110 """
94 """
111
95
112 @abc.abstractmethod
96 def heads():
113 def heads(self):
114 """Determine all known head revisions in the peer.
97 """Determine all known head revisions in the peer.
115
98
116 Returns an iterable of binary nodes.
99 Returns an iterable of binary nodes.
117 """
100 """
118
101
119 @abc.abstractmethod
102 def known(nodes):
120 def known(self, nodes):
121 """Determine whether multiple nodes are known.
103 """Determine whether multiple nodes are known.
122
104
123 Accepts an iterable of nodes whose presence to check for.
105 Accepts an iterable of nodes whose presence to check for.
124
106
125 Returns an iterable of booleans indicating of the corresponding node
107 Returns an iterable of booleans indicating of the corresponding node
126 at that index is known to the peer.
108 at that index is known to the peer.
127 """
109 """
128
110
129 @abc.abstractmethod
111 def listkeys(namespace):
130 def listkeys(self, namespace):
131 """Obtain all keys in a pushkey namespace.
112 """Obtain all keys in a pushkey namespace.
132
113
133 Returns an iterable of key names.
114 Returns an iterable of key names.
134 """
115 """
135
116
136 @abc.abstractmethod
117 def lookup(key):
137 def lookup(self, key):
138 """Resolve a value to a known revision.
118 """Resolve a value to a known revision.
139
119
140 Returns a binary node of the resolved revision on success.
120 Returns a binary node of the resolved revision on success.
141 """
121 """
142
122
143 @abc.abstractmethod
123 def pushkey(namespace, key, old, new):
144 def pushkey(self, namespace, key, old, new):
145 """Set a value using the ``pushkey`` protocol.
124 """Set a value using the ``pushkey`` protocol.
146
125
147 Arguments correspond to the pushkey namespace and key to operate on and
126 Arguments correspond to the pushkey namespace and key to operate on and
148 the old and new values for that key.
127 the old and new values for that key.
149
128
150 Returns a string with the peer result. The value inside varies by the
129 Returns a string with the peer result. The value inside varies by the
151 namespace.
130 namespace.
152 """
131 """
153
132
154 @abc.abstractmethod
133 def stream_out():
155 def stream_out(self):
156 """Obtain streaming clone data.
134 """Obtain streaming clone data.
157
135
158 Successful result should be a generator of data chunks.
136 Successful result should be a generator of data chunks.
159 """
137 """
160
138
161 @abc.abstractmethod
139 def unbundle(bundle, heads, url):
162 def unbundle(self, bundle, heads, url):
163 """Transfer repository data to the peer.
140 """Transfer repository data to the peer.
164
141
165 This is how the bulk of data during a push is transferred.
142 This is how the bulk of data during a push is transferred.
166
143
167 Returns the integer number of heads added to the peer.
144 Returns the integer number of heads added to the peer.
168 """
145 """
169
146
170 class _baselegacywirecommands(object):
147 class ipeerlegacycommands(zi.Interface):
171 """Interface for implementing support for legacy wire protocol commands.
148 """Interface for implementing support for legacy wire protocol commands.
172
149
173 Wire protocol commands transition to legacy status when they are no longer
150 Wire protocol commands transition to legacy status when they are no longer
174 used by modern clients. To facilitate identifying which commands are
151 used by modern clients. To facilitate identifying which commands are
175 legacy, the interfaces are split.
152 legacy, the interfaces are split.
176 """
153 """
177 __metaclass__ = abc.ABCMeta
178
154
179 @abc.abstractmethod
155 def between(pairs):
180 def between(self, pairs):
181 """Obtain nodes between pairs of nodes.
156 """Obtain nodes between pairs of nodes.
182
157
183 ``pairs`` is an iterable of node pairs.
158 ``pairs`` is an iterable of node pairs.
184
159
185 Returns an iterable of iterables of nodes corresponding to each
160 Returns an iterable of iterables of nodes corresponding to each
186 requested pair.
161 requested pair.
187 """
162 """
188
163
189 @abc.abstractmethod
164 def branches(nodes):
190 def branches(self, nodes):
191 """Obtain ancestor changesets of specific nodes back to a branch point.
165 """Obtain ancestor changesets of specific nodes back to a branch point.
192
166
193 For each requested node, the peer finds the first ancestor node that is
167 For each requested node, the peer finds the first ancestor node that is
194 a DAG root or is a merge.
168 a DAG root or is a merge.
195
169
196 Returns an iterable of iterables with the resolved values for each node.
170 Returns an iterable of iterables with the resolved values for each node.
197 """
171 """
198
172
199 @abc.abstractmethod
173 def changegroup(nodes, kind):
200 def changegroup(self, nodes, kind):
201 """Obtain a changegroup with data for descendants of specified nodes."""
174 """Obtain a changegroup with data for descendants of specified nodes."""
202
175
203 @abc.abstractmethod
176 def changegroupsubset(bases, heads, kind):
204 def changegroupsubset(self, bases, heads, kind):
205 pass
177 pass
206
178
207 class peer(_basepeer, _basewirecommands):
179 class ipeerbase(ipeerconnection, ipeercommands):
208 """Unified interface and base class for peer repositories.
180 """Unified interface for peer repositories.
209
181
210 All peer instances must inherit from this class and conform to its
182 All peer instances must conform to this interface.
211 interface.
212 """
183 """
213
184 def iterbatch():
214 @abc.abstractmethod
215 def iterbatch(self):
216 """Obtain an object to be used for multiple method calls.
185 """Obtain an object to be used for multiple method calls.
217
186
218 Various operations call several methods on peer instances. If each
187 Various operations call several methods on peer instances. If each
219 method call were performed immediately and serially, this would
188 method call were performed immediately and serially, this would
220 require round trips to remote peers and/or would slow down execution.
189 require round trips to remote peers and/or would slow down execution.
221
190
222 Some peers have the ability to "batch" method calls to avoid costly
191 Some peers have the ability to "batch" method calls to avoid costly
223 round trips or to facilitate concurrent execution.
192 round trips or to facilitate concurrent execution.
224
193
225 This method returns an object that can be used to indicate intent to
194 This method returns an object that can be used to indicate intent to
226 perform batched method calls.
195 perform batched method calls.
227
196
228 The returned object is a proxy of this peer. It intercepts calls to
197 The returned object is a proxy of this peer. It intercepts calls to
229 batchable methods and queues them instead of performing them
198 batchable methods and queues them instead of performing them
230 immediately. This proxy object has a ``submit`` method that will
199 immediately. This proxy object has a ``submit`` method that will
231 perform all queued batchable method calls. A ``results()`` method
200 perform all queued batchable method calls. A ``results()`` method
232 exposes the results of queued/batched method calls. It is a generator
201 exposes the results of queued/batched method calls. It is a generator
233 of results in the order they were called.
202 of results in the order they were called.
234
203
235 Not all peers or wire protocol implementations may actually batch method
204 Not all peers or wire protocol implementations may actually batch method
236 calls. However, they must all support this API.
205 calls. However, they must all support this API.
237 """
206 """
238
207
239 def capable(self, name):
208 def capable(name):
240 """Determine support for a named capability.
209 """Determine support for a named capability.
241
210
242 Returns ``False`` if capability not supported.
211 Returns ``False`` if capability not supported.
243
212
244 Returns ``True`` if boolean capability is supported. Returns a string
213 Returns ``True`` if boolean capability is supported. Returns a string
245 if capability support is non-boolean.
214 if capability support is non-boolean.
246 """
215 """
216
217 def requirecap(name, purpose):
218 """Require a capability to be present.
219
220 Raises a ``CapabilityError`` if the capability isn't present.
221 """
222
223 class ipeerbaselegacycommands(ipeerbase, ipeerlegacycommands):
224 """Unified peer interface that supports legacy commands."""
225
226 @zi.implementer(ipeerbase)
227 class peer(object):
228 """Base class for peer repositories."""
229
230 def capable(self, name):
247 caps = self.capabilities()
231 caps = self.capabilities()
248 if name in caps:
232 if name in caps:
249 return True
233 return True
250
234
251 name = '%s=' % name
235 name = '%s=' % name
252 for cap in caps:
236 for cap in caps:
253 if cap.startswith(name):
237 if cap.startswith(name):
254 return cap[len(name):]
238 return cap[len(name):]
255
239
256 return False
240 return False
257
241
258 def requirecap(self, name, purpose):
242 def requirecap(self, name, purpose):
259 """Require a capability to be present.
260
261 Raises a ``CapabilityError`` if the capability isn't present.
262 """
263 if self.capable(name):
243 if self.capable(name):
264 return
244 return
265
245
266 raise error.CapabilityError(
246 raise error.CapabilityError(
267 _('cannot %s; remote repository does not support the %r '
247 _('cannot %s; remote repository does not support the %r '
268 'capability') % (purpose, name))
248 'capability') % (purpose, name))
269
249
270 class legacypeer(peer, _baselegacywirecommands):
250 @zi.implementer(ipeerbaselegacycommands)
251 class legacypeer(peer):
271 """peer but with support for legacy wire protocol commands."""
252 """peer but with support for legacy wire protocol commands."""
272
253
273 class completelocalrepository(zi.Interface):
254 class completelocalrepository(zi.Interface):
274 """Monolithic interface for local repositories.
255 """Monolithic interface for local repositories.
275
256
276 This currently captures the reality of things - not how things should be.
257 This currently captures the reality of things - not how things should be.
277 """
258 """
278
259
279 supportedformats = zi.Attribute(
260 supportedformats = zi.Attribute(
280 """Set of requirements that apply to stream clone.
261 """Set of requirements that apply to stream clone.
281
262
282 This is actually a class attribute and is shared among all instances.
263 This is actually a class attribute and is shared among all instances.
283 """)
264 """)
284
265
285 openerreqs = zi.Attribute(
266 openerreqs = zi.Attribute(
286 """Set of requirements that are passed to the opener.
267 """Set of requirements that are passed to the opener.
287
268
288 This is actually a class attribute and is shared among all instances.
269 This is actually a class attribute and is shared among all instances.
289 """)
270 """)
290
271
291 supported = zi.Attribute(
272 supported = zi.Attribute(
292 """Set of requirements that this repo is capable of opening.""")
273 """Set of requirements that this repo is capable of opening.""")
293
274
294 requirements = zi.Attribute(
275 requirements = zi.Attribute(
295 """Set of requirements this repo uses.""")
276 """Set of requirements this repo uses.""")
296
277
297 filtername = zi.Attribute(
278 filtername = zi.Attribute(
298 """Name of the repoview that is active on this repo.""")
279 """Name of the repoview that is active on this repo.""")
299
280
300 wvfs = zi.Attribute(
281 wvfs = zi.Attribute(
301 """VFS used to access the working directory.""")
282 """VFS used to access the working directory.""")
302
283
303 vfs = zi.Attribute(
284 vfs = zi.Attribute(
304 """VFS rooted at the .hg directory.
285 """VFS rooted at the .hg directory.
305
286
306 Used to access repository data not in the store.
287 Used to access repository data not in the store.
307 """)
288 """)
308
289
309 svfs = zi.Attribute(
290 svfs = zi.Attribute(
310 """VFS rooted at the store.
291 """VFS rooted at the store.
311
292
312 Used to access repository data in the store. Typically .hg/store.
293 Used to access repository data in the store. Typically .hg/store.
313 But can point elsewhere if the store is shared.
294 But can point elsewhere if the store is shared.
314 """)
295 """)
315
296
316 root = zi.Attribute(
297 root = zi.Attribute(
317 """Path to the root of the working directory.""")
298 """Path to the root of the working directory.""")
318
299
319 path = zi.Attribute(
300 path = zi.Attribute(
320 """Path to the .hg directory.""")
301 """Path to the .hg directory.""")
321
302
322 origroot = zi.Attribute(
303 origroot = zi.Attribute(
323 """The filesystem path that was used to construct the repo.""")
304 """The filesystem path that was used to construct the repo.""")
324
305
325 auditor = zi.Attribute(
306 auditor = zi.Attribute(
326 """A pathauditor for the working directory.
307 """A pathauditor for the working directory.
327
308
328 This checks if a path refers to a nested repository.
309 This checks if a path refers to a nested repository.
329
310
330 Operates on the filesystem.
311 Operates on the filesystem.
331 """)
312 """)
332
313
333 nofsauditor = zi.Attribute(
314 nofsauditor = zi.Attribute(
334 """A pathauditor for the working directory.
315 """A pathauditor for the working directory.
335
316
336 This is like ``auditor`` except it doesn't do filesystem checks.
317 This is like ``auditor`` except it doesn't do filesystem checks.
337 """)
318 """)
338
319
339 baseui = zi.Attribute(
320 baseui = zi.Attribute(
340 """Original ui instance passed into constructor.""")
321 """Original ui instance passed into constructor.""")
341
322
342 ui = zi.Attribute(
323 ui = zi.Attribute(
343 """Main ui instance for this instance.""")
324 """Main ui instance for this instance.""")
344
325
345 sharedpath = zi.Attribute(
326 sharedpath = zi.Attribute(
346 """Path to the .hg directory of the repo this repo was shared from.""")
327 """Path to the .hg directory of the repo this repo was shared from.""")
347
328
348 store = zi.Attribute(
329 store = zi.Attribute(
349 """A store instance.""")
330 """A store instance.""")
350
331
351 spath = zi.Attribute(
332 spath = zi.Attribute(
352 """Path to the store.""")
333 """Path to the store.""")
353
334
354 sjoin = zi.Attribute(
335 sjoin = zi.Attribute(
355 """Alias to self.store.join.""")
336 """Alias to self.store.join.""")
356
337
357 cachevfs = zi.Attribute(
338 cachevfs = zi.Attribute(
358 """A VFS used to access the cache directory.
339 """A VFS used to access the cache directory.
359
340
360 Typically .hg/cache.
341 Typically .hg/cache.
361 """)
342 """)
362
343
363 filteredrevcache = zi.Attribute(
344 filteredrevcache = zi.Attribute(
364 """Holds sets of revisions to be filtered.""")
345 """Holds sets of revisions to be filtered.""")
365
346
366 names = zi.Attribute(
347 names = zi.Attribute(
367 """A ``namespaces`` instance.""")
348 """A ``namespaces`` instance.""")
368
349
369 def close():
350 def close():
370 """Close the handle on this repository."""
351 """Close the handle on this repository."""
371
352
372 def peer():
353 def peer():
373 """Obtain an object conforming to the ``peer`` interface."""
354 """Obtain an object conforming to the ``peer`` interface."""
374
355
375 def unfiltered():
356 def unfiltered():
376 """Obtain an unfiltered/raw view of this repo."""
357 """Obtain an unfiltered/raw view of this repo."""
377
358
378 def filtered(name, visibilityexceptions=None):
359 def filtered(name, visibilityexceptions=None):
379 """Obtain a named view of this repository."""
360 """Obtain a named view of this repository."""
380
361
381 obsstore = zi.Attribute(
362 obsstore = zi.Attribute(
382 """A store of obsolescence data.""")
363 """A store of obsolescence data.""")
383
364
384 changelog = zi.Attribute(
365 changelog = zi.Attribute(
385 """A handle on the changelog revlog.""")
366 """A handle on the changelog revlog.""")
386
367
387 manifestlog = zi.Attribute(
368 manifestlog = zi.Attribute(
388 """A handle on the root manifest revlog.""")
369 """A handle on the root manifest revlog.""")
389
370
390 dirstate = zi.Attribute(
371 dirstate = zi.Attribute(
391 """Working directory state.""")
372 """Working directory state.""")
392
373
393 narrowpats = zi.Attribute(
374 narrowpats = zi.Attribute(
394 """Matcher patterns for this repository's narrowspec.""")
375 """Matcher patterns for this repository's narrowspec.""")
395
376
396 def narrowmatch():
377 def narrowmatch():
397 """Obtain a matcher for the narrowspec."""
378 """Obtain a matcher for the narrowspec."""
398
379
399 def setnarrowpats(newincludes, newexcludes):
380 def setnarrowpats(newincludes, newexcludes):
400 """Define the narrowspec for this repository."""
381 """Define the narrowspec for this repository."""
401
382
402 def __getitem__(changeid):
383 def __getitem__(changeid):
403 """Try to resolve a changectx."""
384 """Try to resolve a changectx."""
404
385
405 def __contains__(changeid):
386 def __contains__(changeid):
406 """Whether a changeset exists."""
387 """Whether a changeset exists."""
407
388
408 def __nonzero__():
389 def __nonzero__():
409 """Always returns True."""
390 """Always returns True."""
410 return True
391 return True
411
392
412 __bool__ = __nonzero__
393 __bool__ = __nonzero__
413
394
414 def __len__():
395 def __len__():
415 """Returns the number of changesets in the repo."""
396 """Returns the number of changesets in the repo."""
416
397
417 def __iter__():
398 def __iter__():
418 """Iterate over revisions in the changelog."""
399 """Iterate over revisions in the changelog."""
419
400
420 def revs(expr, *args):
401 def revs(expr, *args):
421 """Evaluate a revset.
402 """Evaluate a revset.
422
403
423 Emits revisions.
404 Emits revisions.
424 """
405 """
425
406
426 def set(expr, *args):
407 def set(expr, *args):
427 """Evaluate a revset.
408 """Evaluate a revset.
428
409
429 Emits changectx instances.
410 Emits changectx instances.
430 """
411 """
431
412
432 def anyrevs(specs, user=False, localalias=None):
413 def anyrevs(specs, user=False, localalias=None):
433 """Find revisions matching one of the given revsets."""
414 """Find revisions matching one of the given revsets."""
434
415
435 def url():
416 def url():
436 """Returns a string representing the location of this repo."""
417 """Returns a string representing the location of this repo."""
437
418
438 def hook(name, throw=False, **args):
419 def hook(name, throw=False, **args):
439 """Call a hook."""
420 """Call a hook."""
440
421
441 def tags():
422 def tags():
442 """Return a mapping of tag to node."""
423 """Return a mapping of tag to node."""
443
424
444 def tagtype(tagname):
425 def tagtype(tagname):
445 """Return the type of a given tag."""
426 """Return the type of a given tag."""
446
427
447 def tagslist():
428 def tagslist():
448 """Return a list of tags ordered by revision."""
429 """Return a list of tags ordered by revision."""
449
430
450 def nodetags(node):
431 def nodetags(node):
451 """Return the tags associated with a node."""
432 """Return the tags associated with a node."""
452
433
453 def nodebookmarks(node):
434 def nodebookmarks(node):
454 """Return the list of bookmarks pointing to the specified node."""
435 """Return the list of bookmarks pointing to the specified node."""
455
436
456 def branchmap():
437 def branchmap():
457 """Return a mapping of branch to heads in that branch."""
438 """Return a mapping of branch to heads in that branch."""
458
439
459 def revbranchcache():
440 def revbranchcache():
460 pass
441 pass
461
442
462 def branchtip(branchtip, ignoremissing=False):
443 def branchtip(branchtip, ignoremissing=False):
463 """Return the tip node for a given branch."""
444 """Return the tip node for a given branch."""
464
445
465 def lookup(key):
446 def lookup(key):
466 """Resolve the node for a revision."""
447 """Resolve the node for a revision."""
467
448
468 def lookupbranch(key, remote=None):
449 def lookupbranch(key, remote=None):
469 """Look up the branch name of the given revision or branch name."""
450 """Look up the branch name of the given revision or branch name."""
470
451
471 def known(nodes):
452 def known(nodes):
472 """Determine whether a series of nodes is known.
453 """Determine whether a series of nodes is known.
473
454
474 Returns a list of bools.
455 Returns a list of bools.
475 """
456 """
476
457
477 def local():
458 def local():
478 """Whether the repository is local."""
459 """Whether the repository is local."""
479 return True
460 return True
480
461
481 def publishing():
462 def publishing():
482 """Whether the repository is a publishing repository."""
463 """Whether the repository is a publishing repository."""
483
464
484 def cancopy():
465 def cancopy():
485 pass
466 pass
486
467
487 def shared():
468 def shared():
488 """The type of shared repository or None."""
469 """The type of shared repository or None."""
489
470
490 def wjoin(f, *insidef):
471 def wjoin(f, *insidef):
491 """Calls self.vfs.reljoin(self.root, f, *insidef)"""
472 """Calls self.vfs.reljoin(self.root, f, *insidef)"""
492
473
493 def file(f):
474 def file(f):
494 """Obtain a filelog for a tracked path."""
475 """Obtain a filelog for a tracked path."""
495
476
496 def setparents(p1, p2):
477 def setparents(p1, p2):
497 """Set the parent nodes of the working directory."""
478 """Set the parent nodes of the working directory."""
498
479
499 def filectx(path, changeid=None, fileid=None):
480 def filectx(path, changeid=None, fileid=None):
500 """Obtain a filectx for the given file revision."""
481 """Obtain a filectx for the given file revision."""
501
482
502 def getcwd():
483 def getcwd():
503 """Obtain the current working directory from the dirstate."""
484 """Obtain the current working directory from the dirstate."""
504
485
505 def pathto(f, cwd=None):
486 def pathto(f, cwd=None):
506 """Obtain the relative path to a file."""
487 """Obtain the relative path to a file."""
507
488
508 def adddatafilter(name, fltr):
489 def adddatafilter(name, fltr):
509 pass
490 pass
510
491
511 def wread(filename):
492 def wread(filename):
512 """Read a file from wvfs, using data filters."""
493 """Read a file from wvfs, using data filters."""
513
494
514 def wwrite(filename, data, flags, backgroundclose=False, **kwargs):
495 def wwrite(filename, data, flags, backgroundclose=False, **kwargs):
515 """Write data to a file in the wvfs, using data filters."""
496 """Write data to a file in the wvfs, using data filters."""
516
497
517 def wwritedata(filename, data):
498 def wwritedata(filename, data):
518 """Resolve data for writing to the wvfs, using data filters."""
499 """Resolve data for writing to the wvfs, using data filters."""
519
500
520 def currenttransaction():
501 def currenttransaction():
521 """Obtain the current transaction instance or None."""
502 """Obtain the current transaction instance or None."""
522
503
523 def transaction(desc, report=None):
504 def transaction(desc, report=None):
524 """Open a new transaction to write to the repository."""
505 """Open a new transaction to write to the repository."""
525
506
526 def undofiles():
507 def undofiles():
527 """Returns a list of (vfs, path) for files to undo transactions."""
508 """Returns a list of (vfs, path) for files to undo transactions."""
528
509
529 def recover():
510 def recover():
530 """Roll back an interrupted transaction."""
511 """Roll back an interrupted transaction."""
531
512
532 def rollback(dryrun=False, force=False):
513 def rollback(dryrun=False, force=False):
533 """Undo the last transaction.
514 """Undo the last transaction.
534
515
535 DANGEROUS.
516 DANGEROUS.
536 """
517 """
537
518
538 def updatecaches(tr=None, full=False):
519 def updatecaches(tr=None, full=False):
539 """Warm repo caches."""
520 """Warm repo caches."""
540
521
541 def invalidatecaches():
522 def invalidatecaches():
542 """Invalidate cached data due to the repository mutating."""
523 """Invalidate cached data due to the repository mutating."""
543
524
544 def invalidatevolatilesets():
525 def invalidatevolatilesets():
545 pass
526 pass
546
527
547 def invalidatedirstate():
528 def invalidatedirstate():
548 """Invalidate the dirstate."""
529 """Invalidate the dirstate."""
549
530
550 def invalidate(clearfilecache=False):
531 def invalidate(clearfilecache=False):
551 pass
532 pass
552
533
553 def invalidateall():
534 def invalidateall():
554 pass
535 pass
555
536
556 def lock(wait=True):
537 def lock(wait=True):
557 """Lock the repository store and return a lock instance."""
538 """Lock the repository store and return a lock instance."""
558
539
559 def wlock(wait=True):
540 def wlock(wait=True):
560 """Lock the non-store parts of the repository."""
541 """Lock the non-store parts of the repository."""
561
542
562 def currentwlock():
543 def currentwlock():
563 """Return the wlock if it's held or None."""
544 """Return the wlock if it's held or None."""
564
545
565 def checkcommitpatterns(wctx, vdirs, match, status, fail):
546 def checkcommitpatterns(wctx, vdirs, match, status, fail):
566 pass
547 pass
567
548
568 def commit(text='', user=None, date=None, match=None, force=False,
549 def commit(text='', user=None, date=None, match=None, force=False,
569 editor=False, extra=None):
550 editor=False, extra=None):
570 """Add a new revision to the repository."""
551 """Add a new revision to the repository."""
571
552
572 def commitctx(ctx, error=False):
553 def commitctx(ctx, error=False):
573 """Commit a commitctx instance to the repository."""
554 """Commit a commitctx instance to the repository."""
574
555
575 def destroying():
556 def destroying():
576 """Inform the repository that nodes are about to be destroyed."""
557 """Inform the repository that nodes are about to be destroyed."""
577
558
578 def destroyed():
559 def destroyed():
579 """Inform the repository that nodes have been destroyed."""
560 """Inform the repository that nodes have been destroyed."""
580
561
581 def status(node1='.', node2=None, match=None, ignored=False,
562 def status(node1='.', node2=None, match=None, ignored=False,
582 clean=False, unknown=False, listsubrepos=False):
563 clean=False, unknown=False, listsubrepos=False):
583 """Convenience method to call repo[x].status()."""
564 """Convenience method to call repo[x].status()."""
584
565
585 def addpostdsstatus(ps):
566 def addpostdsstatus(ps):
586 pass
567 pass
587
568
588 def postdsstatus():
569 def postdsstatus():
589 pass
570 pass
590
571
591 def clearpostdsstatus():
572 def clearpostdsstatus():
592 pass
573 pass
593
574
594 def heads(start=None):
575 def heads(start=None):
595 """Obtain list of nodes that are DAG heads."""
576 """Obtain list of nodes that are DAG heads."""
596
577
597 def branchheads(branch=None, start=None, closed=False):
578 def branchheads(branch=None, start=None, closed=False):
598 pass
579 pass
599
580
600 def branches(nodes):
581 def branches(nodes):
601 pass
582 pass
602
583
603 def between(pairs):
584 def between(pairs):
604 pass
585 pass
605
586
606 def checkpush(pushop):
587 def checkpush(pushop):
607 pass
588 pass
608
589
609 prepushoutgoinghooks = zi.Attribute(
590 prepushoutgoinghooks = zi.Attribute(
610 """util.hooks instance.""")
591 """util.hooks instance.""")
611
592
612 def pushkey(namespace, key, old, new):
593 def pushkey(namespace, key, old, new):
613 pass
594 pass
614
595
615 def listkeys(namespace):
596 def listkeys(namespace):
616 pass
597 pass
617
598
618 def debugwireargs(one, two, three=None, four=None, five=None):
599 def debugwireargs(one, two, three=None, four=None, five=None):
619 pass
600 pass
620
601
621 def savecommitmessage(text):
602 def savecommitmessage(text):
622 pass
603 pass
@@ -1,616 +1,616 b''
1 # sshpeer.py - ssh repository proxy class for mercurial
1 # sshpeer.py - ssh repository proxy class for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 re
10 import re
11 import uuid
11 import uuid
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 wireproto,
18 wireproto,
19 wireprotoserver,
19 wireprotoserver,
20 wireprototypes,
20 wireprototypes,
21 )
21 )
22 from .utils import (
22 from .utils import (
23 procutil,
23 procutil,
24 )
24 )
25
25
26 def _serverquote(s):
26 def _serverquote(s):
27 """quote a string for the remote shell ... which we assume is sh"""
27 """quote a string for the remote shell ... which we assume is sh"""
28 if not s:
28 if not s:
29 return s
29 return s
30 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
30 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
31 return s
31 return s
32 return "'%s'" % s.replace("'", "'\\''")
32 return "'%s'" % s.replace("'", "'\\''")
33
33
34 def _forwardoutput(ui, pipe):
34 def _forwardoutput(ui, pipe):
35 """display all data currently available on pipe as remote output.
35 """display all data currently available on pipe as remote output.
36
36
37 This is non blocking."""
37 This is non blocking."""
38 if pipe:
38 if pipe:
39 s = procutil.readpipe(pipe)
39 s = procutil.readpipe(pipe)
40 if s:
40 if s:
41 for l in s.splitlines():
41 for l in s.splitlines():
42 ui.status(_("remote: "), l, '\n')
42 ui.status(_("remote: "), l, '\n')
43
43
44 class doublepipe(object):
44 class doublepipe(object):
45 """Operate a side-channel pipe in addition of a main one
45 """Operate a side-channel pipe in addition of a main one
46
46
47 The side-channel pipe contains server output to be forwarded to the user
47 The side-channel pipe contains server output to be forwarded to the user
48 input. The double pipe will behave as the "main" pipe, but will ensure the
48 input. The double pipe will behave as the "main" pipe, but will ensure the
49 content of the "side" pipe is properly processed while we wait for blocking
49 content of the "side" pipe is properly processed while we wait for blocking
50 call on the "main" pipe.
50 call on the "main" pipe.
51
51
52 If large amounts of data are read from "main", the forward will cease after
52 If large amounts of data are read from "main", the forward will cease after
53 the first bytes start to appear. This simplifies the implementation
53 the first bytes start to appear. This simplifies the implementation
54 without affecting actual output of sshpeer too much as we rarely issue
54 without affecting actual output of sshpeer too much as we rarely issue
55 large read for data not yet emitted by the server.
55 large read for data not yet emitted by the server.
56
56
57 The main pipe is expected to be a 'bufferedinputpipe' from the util module
57 The main pipe is expected to be a 'bufferedinputpipe' from the util module
58 that handle all the os specific bits. This class lives in this module
58 that handle all the os specific bits. This class lives in this module
59 because it focus on behavior specific to the ssh protocol."""
59 because it focus on behavior specific to the ssh protocol."""
60
60
61 def __init__(self, ui, main, side):
61 def __init__(self, ui, main, side):
62 self._ui = ui
62 self._ui = ui
63 self._main = main
63 self._main = main
64 self._side = side
64 self._side = side
65
65
66 def _wait(self):
66 def _wait(self):
67 """wait until some data are available on main or side
67 """wait until some data are available on main or side
68
68
69 return a pair of boolean (ismainready, issideready)
69 return a pair of boolean (ismainready, issideready)
70
70
71 (This will only wait for data if the setup is supported by `util.poll`)
71 (This will only wait for data if the setup is supported by `util.poll`)
72 """
72 """
73 if (isinstance(self._main, util.bufferedinputpipe) and
73 if (isinstance(self._main, util.bufferedinputpipe) and
74 self._main.hasbuffer):
74 self._main.hasbuffer):
75 # Main has data. Assume side is worth poking at.
75 # Main has data. Assume side is worth poking at.
76 return True, True
76 return True, True
77
77
78 fds = [self._main.fileno(), self._side.fileno()]
78 fds = [self._main.fileno(), self._side.fileno()]
79 try:
79 try:
80 act = util.poll(fds)
80 act = util.poll(fds)
81 except NotImplementedError:
81 except NotImplementedError:
82 # non supported yet case, assume all have data.
82 # non supported yet case, assume all have data.
83 act = fds
83 act = fds
84 return (self._main.fileno() in act, self._side.fileno() in act)
84 return (self._main.fileno() in act, self._side.fileno() in act)
85
85
86 def write(self, data):
86 def write(self, data):
87 return self._call('write', data)
87 return self._call('write', data)
88
88
89 def read(self, size):
89 def read(self, size):
90 r = self._call('read', size)
90 r = self._call('read', size)
91 if size != 0 and not r:
91 if size != 0 and not r:
92 # We've observed a condition that indicates the
92 # We've observed a condition that indicates the
93 # stdout closed unexpectedly. Check stderr one
93 # stdout closed unexpectedly. Check stderr one
94 # more time and snag anything that's there before
94 # more time and snag anything that's there before
95 # letting anyone know the main part of the pipe
95 # letting anyone know the main part of the pipe
96 # closed prematurely.
96 # closed prematurely.
97 _forwardoutput(self._ui, self._side)
97 _forwardoutput(self._ui, self._side)
98 return r
98 return r
99
99
100 def readline(self):
100 def readline(self):
101 return self._call('readline')
101 return self._call('readline')
102
102
103 def _call(self, methname, data=None):
103 def _call(self, methname, data=None):
104 """call <methname> on "main", forward output of "side" while blocking
104 """call <methname> on "main", forward output of "side" while blocking
105 """
105 """
106 # data can be '' or 0
106 # data can be '' or 0
107 if (data is not None and not data) or self._main.closed:
107 if (data is not None and not data) or self._main.closed:
108 _forwardoutput(self._ui, self._side)
108 _forwardoutput(self._ui, self._side)
109 return ''
109 return ''
110 while True:
110 while True:
111 mainready, sideready = self._wait()
111 mainready, sideready = self._wait()
112 if sideready:
112 if sideready:
113 _forwardoutput(self._ui, self._side)
113 _forwardoutput(self._ui, self._side)
114 if mainready:
114 if mainready:
115 meth = getattr(self._main, methname)
115 meth = getattr(self._main, methname)
116 if data is None:
116 if data is None:
117 return meth()
117 return meth()
118 else:
118 else:
119 return meth(data)
119 return meth(data)
120
120
121 def close(self):
121 def close(self):
122 return self._main.close()
122 return self._main.close()
123
123
124 def flush(self):
124 def flush(self):
125 return self._main.flush()
125 return self._main.flush()
126
126
127 def _cleanuppipes(ui, pipei, pipeo, pipee):
127 def _cleanuppipes(ui, pipei, pipeo, pipee):
128 """Clean up pipes used by an SSH connection."""
128 """Clean up pipes used by an SSH connection."""
129 if pipeo:
129 if pipeo:
130 pipeo.close()
130 pipeo.close()
131 if pipei:
131 if pipei:
132 pipei.close()
132 pipei.close()
133
133
134 if pipee:
134 if pipee:
135 # Try to read from the err descriptor until EOF.
135 # Try to read from the err descriptor until EOF.
136 try:
136 try:
137 for l in pipee:
137 for l in pipee:
138 ui.status(_('remote: '), l)
138 ui.status(_('remote: '), l)
139 except (IOError, ValueError):
139 except (IOError, ValueError):
140 pass
140 pass
141
141
142 pipee.close()
142 pipee.close()
143
143
144 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
144 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
145 """Create an SSH connection to a server.
145 """Create an SSH connection to a server.
146
146
147 Returns a tuple of (process, stdin, stdout, stderr) for the
147 Returns a tuple of (process, stdin, stdout, stderr) for the
148 spawned process.
148 spawned process.
149 """
149 """
150 cmd = '%s %s %s' % (
150 cmd = '%s %s %s' % (
151 sshcmd,
151 sshcmd,
152 args,
152 args,
153 procutil.shellquote('%s -R %s serve --stdio' % (
153 procutil.shellquote('%s -R %s serve --stdio' % (
154 _serverquote(remotecmd), _serverquote(path))))
154 _serverquote(remotecmd), _serverquote(path))))
155
155
156 ui.debug('running %s\n' % cmd)
156 ui.debug('running %s\n' % cmd)
157 cmd = procutil.quotecommand(cmd)
157 cmd = procutil.quotecommand(cmd)
158
158
159 # no buffer allow the use of 'select'
159 # no buffer allow the use of 'select'
160 # feel free to remove buffering and select usage when we ultimately
160 # feel free to remove buffering and select usage when we ultimately
161 # move to threading.
161 # move to threading.
162 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
162 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
163
163
164 return proc, stdin, stdout, stderr
164 return proc, stdin, stdout, stderr
165
165
166 def _performhandshake(ui, stdin, stdout, stderr):
166 def _performhandshake(ui, stdin, stdout, stderr):
167 def badresponse():
167 def badresponse():
168 # Flush any output on stderr.
168 # Flush any output on stderr.
169 _forwardoutput(ui, stderr)
169 _forwardoutput(ui, stderr)
170
170
171 msg = _('no suitable response from remote hg')
171 msg = _('no suitable response from remote hg')
172 hint = ui.config('ui', 'ssherrorhint')
172 hint = ui.config('ui', 'ssherrorhint')
173 raise error.RepoError(msg, hint=hint)
173 raise error.RepoError(msg, hint=hint)
174
174
175 # The handshake consists of sending wire protocol commands in reverse
175 # The handshake consists of sending wire protocol commands in reverse
176 # order of protocol implementation and then sniffing for a response
176 # order of protocol implementation and then sniffing for a response
177 # to one of them.
177 # to one of them.
178 #
178 #
179 # Those commands (from oldest to newest) are:
179 # Those commands (from oldest to newest) are:
180 #
180 #
181 # ``between``
181 # ``between``
182 # Asks for the set of revisions between a pair of revisions. Command
182 # Asks for the set of revisions between a pair of revisions. Command
183 # present in all Mercurial server implementations.
183 # present in all Mercurial server implementations.
184 #
184 #
185 # ``hello``
185 # ``hello``
186 # Instructs the server to advertise its capabilities. Introduced in
186 # Instructs the server to advertise its capabilities. Introduced in
187 # Mercurial 0.9.1.
187 # Mercurial 0.9.1.
188 #
188 #
189 # ``upgrade``
189 # ``upgrade``
190 # Requests upgrade from default transport protocol version 1 to
190 # Requests upgrade from default transport protocol version 1 to
191 # a newer version. Introduced in Mercurial 4.6 as an experimental
191 # a newer version. Introduced in Mercurial 4.6 as an experimental
192 # feature.
192 # feature.
193 #
193 #
194 # The ``between`` command is issued with a request for the null
194 # The ``between`` command is issued with a request for the null
195 # range. If the remote is a Mercurial server, this request will
195 # range. If the remote is a Mercurial server, this request will
196 # generate a specific response: ``1\n\n``. This represents the
196 # generate a specific response: ``1\n\n``. This represents the
197 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
197 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
198 # in the output stream and know this is the response to ``between``
198 # in the output stream and know this is the response to ``between``
199 # and we're at the end of our handshake reply.
199 # and we're at the end of our handshake reply.
200 #
200 #
201 # The response to the ``hello`` command will be a line with the
201 # The response to the ``hello`` command will be a line with the
202 # length of the value returned by that command followed by that
202 # length of the value returned by that command followed by that
203 # value. If the server doesn't support ``hello`` (which should be
203 # value. If the server doesn't support ``hello`` (which should be
204 # rare), that line will be ``0\n``. Otherwise, the value will contain
204 # rare), that line will be ``0\n``. Otherwise, the value will contain
205 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
205 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
206 # the capabilities of the server.
206 # the capabilities of the server.
207 #
207 #
208 # The ``upgrade`` command isn't really a command in the traditional
208 # The ``upgrade`` command isn't really a command in the traditional
209 # sense of version 1 of the transport because it isn't using the
209 # sense of version 1 of the transport because it isn't using the
210 # proper mechanism for formatting insteads: instead, it just encodes
210 # proper mechanism for formatting insteads: instead, it just encodes
211 # arguments on the line, delimited by spaces.
211 # arguments on the line, delimited by spaces.
212 #
212 #
213 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
213 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
214 # If the server doesn't support protocol upgrades, it will reply to
214 # If the server doesn't support protocol upgrades, it will reply to
215 # this line with ``0\n``. Otherwise, it emits an
215 # this line with ``0\n``. Otherwise, it emits an
216 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
216 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
217 # Content immediately following this line describes additional
217 # Content immediately following this line describes additional
218 # protocol and server state.
218 # protocol and server state.
219 #
219 #
220 # In addition to the responses to our command requests, the server
220 # In addition to the responses to our command requests, the server
221 # may emit "banner" output on stdout. SSH servers are allowed to
221 # may emit "banner" output on stdout. SSH servers are allowed to
222 # print messages to stdout on login. Issuing commands on connection
222 # print messages to stdout on login. Issuing commands on connection
223 # allows us to flush this banner output from the server by scanning
223 # allows us to flush this banner output from the server by scanning
224 # for output to our well-known ``between`` command. Of course, if
224 # for output to our well-known ``between`` command. Of course, if
225 # the banner contains ``1\n\n``, this will throw off our detection.
225 # the banner contains ``1\n\n``, this will throw off our detection.
226
226
227 requestlog = ui.configbool('devel', 'debug.peer-request')
227 requestlog = ui.configbool('devel', 'debug.peer-request')
228
228
229 # Generate a random token to help identify responses to version 2
229 # Generate a random token to help identify responses to version 2
230 # upgrade request.
230 # upgrade request.
231 token = pycompat.sysbytes(str(uuid.uuid4()))
231 token = pycompat.sysbytes(str(uuid.uuid4()))
232 upgradecaps = [
232 upgradecaps = [
233 ('proto', wireprotoserver.SSHV2),
233 ('proto', wireprotoserver.SSHV2),
234 ]
234 ]
235 upgradecaps = util.urlreq.urlencode(upgradecaps)
235 upgradecaps = util.urlreq.urlencode(upgradecaps)
236
236
237 try:
237 try:
238 pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
238 pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
239 handshake = [
239 handshake = [
240 'hello\n',
240 'hello\n',
241 'between\n',
241 'between\n',
242 'pairs %d\n' % len(pairsarg),
242 'pairs %d\n' % len(pairsarg),
243 pairsarg,
243 pairsarg,
244 ]
244 ]
245
245
246 # Request upgrade to version 2 if configured.
246 # Request upgrade to version 2 if configured.
247 if ui.configbool('experimental', 'sshpeer.advertise-v2'):
247 if ui.configbool('experimental', 'sshpeer.advertise-v2'):
248 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
248 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
249 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
249 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
250
250
251 if requestlog:
251 if requestlog:
252 ui.debug('devel-peer-request: hello\n')
252 ui.debug('devel-peer-request: hello\n')
253 ui.debug('sending hello command\n')
253 ui.debug('sending hello command\n')
254 if requestlog:
254 if requestlog:
255 ui.debug('devel-peer-request: between\n')
255 ui.debug('devel-peer-request: between\n')
256 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
256 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
257 ui.debug('sending between command\n')
257 ui.debug('sending between command\n')
258
258
259 stdin.write(''.join(handshake))
259 stdin.write(''.join(handshake))
260 stdin.flush()
260 stdin.flush()
261 except IOError:
261 except IOError:
262 badresponse()
262 badresponse()
263
263
264 # Assume version 1 of wire protocol by default.
264 # Assume version 1 of wire protocol by default.
265 protoname = wireprototypes.SSHV1
265 protoname = wireprototypes.SSHV1
266 reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
266 reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
267
267
268 lines = ['', 'dummy']
268 lines = ['', 'dummy']
269 max_noise = 500
269 max_noise = 500
270 while lines[-1] and max_noise:
270 while lines[-1] and max_noise:
271 try:
271 try:
272 l = stdout.readline()
272 l = stdout.readline()
273 _forwardoutput(ui, stderr)
273 _forwardoutput(ui, stderr)
274
274
275 # Look for reply to protocol upgrade request. It has a token
275 # Look for reply to protocol upgrade request. It has a token
276 # in it, so there should be no false positives.
276 # in it, so there should be no false positives.
277 m = reupgraded.match(l)
277 m = reupgraded.match(l)
278 if m:
278 if m:
279 protoname = m.group(1)
279 protoname = m.group(1)
280 ui.debug('protocol upgraded to %s\n' % protoname)
280 ui.debug('protocol upgraded to %s\n' % protoname)
281 # If an upgrade was handled, the ``hello`` and ``between``
281 # If an upgrade was handled, the ``hello`` and ``between``
282 # requests are ignored. The next output belongs to the
282 # requests are ignored. The next output belongs to the
283 # protocol, so stop scanning lines.
283 # protocol, so stop scanning lines.
284 break
284 break
285
285
286 # Otherwise it could be a banner, ``0\n`` response if server
286 # Otherwise it could be a banner, ``0\n`` response if server
287 # doesn't support upgrade.
287 # doesn't support upgrade.
288
288
289 if lines[-1] == '1\n' and l == '\n':
289 if lines[-1] == '1\n' and l == '\n':
290 break
290 break
291 if l:
291 if l:
292 ui.debug('remote: ', l)
292 ui.debug('remote: ', l)
293 lines.append(l)
293 lines.append(l)
294 max_noise -= 1
294 max_noise -= 1
295 except IOError:
295 except IOError:
296 badresponse()
296 badresponse()
297 else:
297 else:
298 badresponse()
298 badresponse()
299
299
300 caps = set()
300 caps = set()
301
301
302 # For version 1, we should see a ``capabilities`` line in response to the
302 # For version 1, we should see a ``capabilities`` line in response to the
303 # ``hello`` command.
303 # ``hello`` command.
304 if protoname == wireprototypes.SSHV1:
304 if protoname == wireprototypes.SSHV1:
305 for l in reversed(lines):
305 for l in reversed(lines):
306 # Look for response to ``hello`` command. Scan from the back so
306 # Look for response to ``hello`` command. Scan from the back so
307 # we don't misinterpret banner output as the command reply.
307 # we don't misinterpret banner output as the command reply.
308 if l.startswith('capabilities:'):
308 if l.startswith('capabilities:'):
309 caps.update(l[:-1].split(':')[1].split())
309 caps.update(l[:-1].split(':')[1].split())
310 break
310 break
311 elif protoname == wireprotoserver.SSHV2:
311 elif protoname == wireprotoserver.SSHV2:
312 # We see a line with number of bytes to follow and then a value
312 # We see a line with number of bytes to follow and then a value
313 # looking like ``capabilities: *``.
313 # looking like ``capabilities: *``.
314 line = stdout.readline()
314 line = stdout.readline()
315 try:
315 try:
316 valuelen = int(line)
316 valuelen = int(line)
317 except ValueError:
317 except ValueError:
318 badresponse()
318 badresponse()
319
319
320 capsline = stdout.read(valuelen)
320 capsline = stdout.read(valuelen)
321 if not capsline.startswith('capabilities: '):
321 if not capsline.startswith('capabilities: '):
322 badresponse()
322 badresponse()
323
323
324 ui.debug('remote: %s\n' % capsline)
324 ui.debug('remote: %s\n' % capsline)
325
325
326 caps.update(capsline.split(':')[1].split())
326 caps.update(capsline.split(':')[1].split())
327 # Trailing newline.
327 # Trailing newline.
328 stdout.read(1)
328 stdout.read(1)
329
329
330 # Error if we couldn't find capabilities, this means:
330 # Error if we couldn't find capabilities, this means:
331 #
331 #
332 # 1. Remote isn't a Mercurial server
332 # 1. Remote isn't a Mercurial server
333 # 2. Remote is a <0.9.1 Mercurial server
333 # 2. Remote is a <0.9.1 Mercurial server
334 # 3. Remote is a future Mercurial server that dropped ``hello``
334 # 3. Remote is a future Mercurial server that dropped ``hello``
335 # and other attempted handshake mechanisms.
335 # and other attempted handshake mechanisms.
336 if not caps:
336 if not caps:
337 badresponse()
337 badresponse()
338
338
339 # Flush any output on stderr before proceeding.
339 # Flush any output on stderr before proceeding.
340 _forwardoutput(ui, stderr)
340 _forwardoutput(ui, stderr)
341
341
342 return protoname, caps
342 return protoname, caps
343
343
344 class sshv1peer(wireproto.wirepeer):
344 class sshv1peer(wireproto.wirepeer):
345 def __init__(self, ui, url, proc, stdin, stdout, stderr, caps,
345 def __init__(self, ui, url, proc, stdin, stdout, stderr, caps,
346 autoreadstderr=True):
346 autoreadstderr=True):
347 """Create a peer from an existing SSH connection.
347 """Create a peer from an existing SSH connection.
348
348
349 ``proc`` is a handle on the underlying SSH process.
349 ``proc`` is a handle on the underlying SSH process.
350 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
350 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
351 pipes for that process.
351 pipes for that process.
352 ``caps`` is a set of capabilities supported by the remote.
352 ``caps`` is a set of capabilities supported by the remote.
353 ``autoreadstderr`` denotes whether to automatically read from
353 ``autoreadstderr`` denotes whether to automatically read from
354 stderr and to forward its output.
354 stderr and to forward its output.
355 """
355 """
356 self._url = url
356 self._url = url
357 self._ui = ui
357 self._ui = ui
358 # self._subprocess is unused. Keeping a handle on the process
358 # self._subprocess is unused. Keeping a handle on the process
359 # holds a reference and prevents it from being garbage collected.
359 # holds a reference and prevents it from being garbage collected.
360 self._subprocess = proc
360 self._subprocess = proc
361
361
362 # And we hook up our "doublepipe" wrapper to allow querying
362 # And we hook up our "doublepipe" wrapper to allow querying
363 # stderr any time we perform I/O.
363 # stderr any time we perform I/O.
364 if autoreadstderr:
364 if autoreadstderr:
365 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
365 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
366 stdin = doublepipe(ui, stdin, stderr)
366 stdin = doublepipe(ui, stdin, stderr)
367
367
368 self._pipeo = stdin
368 self._pipeo = stdin
369 self._pipei = stdout
369 self._pipei = stdout
370 self._pipee = stderr
370 self._pipee = stderr
371 self._caps = caps
371 self._caps = caps
372 self._autoreadstderr = autoreadstderr
372 self._autoreadstderr = autoreadstderr
373
373
374 # Commands that have a "framed" response where the first line of the
374 # Commands that have a "framed" response where the first line of the
375 # response contains the length of that response.
375 # response contains the length of that response.
376 _FRAMED_COMMANDS = {
376 _FRAMED_COMMANDS = {
377 'batch',
377 'batch',
378 }
378 }
379
379
380 # Begin of _basepeer interface.
380 # Begin of ipeerconnection interface.
381
381
382 @util.propertycache
382 @util.propertycache
383 def ui(self):
383 def ui(self):
384 return self._ui
384 return self._ui
385
385
386 def url(self):
386 def url(self):
387 return self._url
387 return self._url
388
388
389 def local(self):
389 def local(self):
390 return None
390 return None
391
391
392 def peer(self):
392 def peer(self):
393 return self
393 return self
394
394
395 def canpush(self):
395 def canpush(self):
396 return True
396 return True
397
397
398 def close(self):
398 def close(self):
399 pass
399 pass
400
400
401 # End of _basepeer interface.
401 # End of ipeerconnection interface.
402
402
403 # Begin of _basewirecommands interface.
403 # Begin of ipeercommands interface.
404
404
405 def capabilities(self):
405 def capabilities(self):
406 return self._caps
406 return self._caps
407
407
408 # End of _basewirecommands interface.
408 # End of ipeercommands interface.
409
409
410 def _readerr(self):
410 def _readerr(self):
411 _forwardoutput(self.ui, self._pipee)
411 _forwardoutput(self.ui, self._pipee)
412
412
413 def _abort(self, exception):
413 def _abort(self, exception):
414 self._cleanup()
414 self._cleanup()
415 raise exception
415 raise exception
416
416
417 def _cleanup(self):
417 def _cleanup(self):
418 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
418 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
419
419
420 __del__ = _cleanup
420 __del__ = _cleanup
421
421
422 def _sendrequest(self, cmd, args, framed=False):
422 def _sendrequest(self, cmd, args, framed=False):
423 if (self.ui.debugflag
423 if (self.ui.debugflag
424 and self.ui.configbool('devel', 'debug.peer-request')):
424 and self.ui.configbool('devel', 'debug.peer-request')):
425 dbg = self.ui.debug
425 dbg = self.ui.debug
426 line = 'devel-peer-request: %s\n'
426 line = 'devel-peer-request: %s\n'
427 dbg(line % cmd)
427 dbg(line % cmd)
428 for key, value in sorted(args.items()):
428 for key, value in sorted(args.items()):
429 if not isinstance(value, dict):
429 if not isinstance(value, dict):
430 dbg(line % ' %s: %d bytes' % (key, len(value)))
430 dbg(line % ' %s: %d bytes' % (key, len(value)))
431 else:
431 else:
432 for dk, dv in sorted(value.items()):
432 for dk, dv in sorted(value.items()):
433 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
433 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
434 self.ui.debug("sending %s command\n" % cmd)
434 self.ui.debug("sending %s command\n" % cmd)
435 self._pipeo.write("%s\n" % cmd)
435 self._pipeo.write("%s\n" % cmd)
436 _func, names = wireproto.commands[cmd]
436 _func, names = wireproto.commands[cmd]
437 keys = names.split()
437 keys = names.split()
438 wireargs = {}
438 wireargs = {}
439 for k in keys:
439 for k in keys:
440 if k == '*':
440 if k == '*':
441 wireargs['*'] = args
441 wireargs['*'] = args
442 break
442 break
443 else:
443 else:
444 wireargs[k] = args[k]
444 wireargs[k] = args[k]
445 del args[k]
445 del args[k]
446 for k, v in sorted(wireargs.iteritems()):
446 for k, v in sorted(wireargs.iteritems()):
447 self._pipeo.write("%s %d\n" % (k, len(v)))
447 self._pipeo.write("%s %d\n" % (k, len(v)))
448 if isinstance(v, dict):
448 if isinstance(v, dict):
449 for dk, dv in v.iteritems():
449 for dk, dv in v.iteritems():
450 self._pipeo.write("%s %d\n" % (dk, len(dv)))
450 self._pipeo.write("%s %d\n" % (dk, len(dv)))
451 self._pipeo.write(dv)
451 self._pipeo.write(dv)
452 else:
452 else:
453 self._pipeo.write(v)
453 self._pipeo.write(v)
454 self._pipeo.flush()
454 self._pipeo.flush()
455
455
456 # We know exactly how many bytes are in the response. So return a proxy
456 # We know exactly how many bytes are in the response. So return a proxy
457 # around the raw output stream that allows reading exactly this many
457 # around the raw output stream that allows reading exactly this many
458 # bytes. Callers then can read() without fear of overrunning the
458 # bytes. Callers then can read() without fear of overrunning the
459 # response.
459 # response.
460 if framed:
460 if framed:
461 amount = self._getamount()
461 amount = self._getamount()
462 return util.cappedreader(self._pipei, amount)
462 return util.cappedreader(self._pipei, amount)
463
463
464 return self._pipei
464 return self._pipei
465
465
466 def _callstream(self, cmd, **args):
466 def _callstream(self, cmd, **args):
467 args = pycompat.byteskwargs(args)
467 args = pycompat.byteskwargs(args)
468 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
468 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
469
469
470 def _callcompressable(self, cmd, **args):
470 def _callcompressable(self, cmd, **args):
471 args = pycompat.byteskwargs(args)
471 args = pycompat.byteskwargs(args)
472 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
472 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
473
473
474 def _call(self, cmd, **args):
474 def _call(self, cmd, **args):
475 args = pycompat.byteskwargs(args)
475 args = pycompat.byteskwargs(args)
476 return self._sendrequest(cmd, args, framed=True).read()
476 return self._sendrequest(cmd, args, framed=True).read()
477
477
478 def _callpush(self, cmd, fp, **args):
478 def _callpush(self, cmd, fp, **args):
479 # The server responds with an empty frame if the client should
479 # The server responds with an empty frame if the client should
480 # continue submitting the payload.
480 # continue submitting the payload.
481 r = self._call(cmd, **args)
481 r = self._call(cmd, **args)
482 if r:
482 if r:
483 return '', r
483 return '', r
484
484
485 # The payload consists of frames with content followed by an empty
485 # The payload consists of frames with content followed by an empty
486 # frame.
486 # frame.
487 for d in iter(lambda: fp.read(4096), ''):
487 for d in iter(lambda: fp.read(4096), ''):
488 self._writeframed(d)
488 self._writeframed(d)
489 self._writeframed("", flush=True)
489 self._writeframed("", flush=True)
490
490
491 # In case of success, there is an empty frame and a frame containing
491 # In case of success, there is an empty frame and a frame containing
492 # the integer result (as a string).
492 # the integer result (as a string).
493 # In case of error, there is a non-empty frame containing the error.
493 # In case of error, there is a non-empty frame containing the error.
494 r = self._readframed()
494 r = self._readframed()
495 if r:
495 if r:
496 return '', r
496 return '', r
497 return self._readframed(), ''
497 return self._readframed(), ''
498
498
499 def _calltwowaystream(self, cmd, fp, **args):
499 def _calltwowaystream(self, cmd, fp, **args):
500 # The server responds with an empty frame if the client should
500 # The server responds with an empty frame if the client should
501 # continue submitting the payload.
501 # continue submitting the payload.
502 r = self._call(cmd, **args)
502 r = self._call(cmd, **args)
503 if r:
503 if r:
504 # XXX needs to be made better
504 # XXX needs to be made better
505 raise error.Abort(_('unexpected remote reply: %s') % r)
505 raise error.Abort(_('unexpected remote reply: %s') % r)
506
506
507 # The payload consists of frames with content followed by an empty
507 # The payload consists of frames with content followed by an empty
508 # frame.
508 # frame.
509 for d in iter(lambda: fp.read(4096), ''):
509 for d in iter(lambda: fp.read(4096), ''):
510 self._writeframed(d)
510 self._writeframed(d)
511 self._writeframed("", flush=True)
511 self._writeframed("", flush=True)
512
512
513 return self._pipei
513 return self._pipei
514
514
515 def _getamount(self):
515 def _getamount(self):
516 l = self._pipei.readline()
516 l = self._pipei.readline()
517 if l == '\n':
517 if l == '\n':
518 if self._autoreadstderr:
518 if self._autoreadstderr:
519 self._readerr()
519 self._readerr()
520 msg = _('check previous remote output')
520 msg = _('check previous remote output')
521 self._abort(error.OutOfBandError(hint=msg))
521 self._abort(error.OutOfBandError(hint=msg))
522 if self._autoreadstderr:
522 if self._autoreadstderr:
523 self._readerr()
523 self._readerr()
524 try:
524 try:
525 return int(l)
525 return int(l)
526 except ValueError:
526 except ValueError:
527 self._abort(error.ResponseError(_("unexpected response:"), l))
527 self._abort(error.ResponseError(_("unexpected response:"), l))
528
528
529 def _readframed(self):
529 def _readframed(self):
530 size = self._getamount()
530 size = self._getamount()
531 if not size:
531 if not size:
532 return b''
532 return b''
533
533
534 return self._pipei.read(size)
534 return self._pipei.read(size)
535
535
536 def _writeframed(self, data, flush=False):
536 def _writeframed(self, data, flush=False):
537 self._pipeo.write("%d\n" % len(data))
537 self._pipeo.write("%d\n" % len(data))
538 if data:
538 if data:
539 self._pipeo.write(data)
539 self._pipeo.write(data)
540 if flush:
540 if flush:
541 self._pipeo.flush()
541 self._pipeo.flush()
542 if self._autoreadstderr:
542 if self._autoreadstderr:
543 self._readerr()
543 self._readerr()
544
544
545 class sshv2peer(sshv1peer):
545 class sshv2peer(sshv1peer):
546 """A peer that speakers version 2 of the transport protocol."""
546 """A peer that speakers version 2 of the transport protocol."""
547 # Currently version 2 is identical to version 1 post handshake.
547 # Currently version 2 is identical to version 1 post handshake.
548 # And handshake is performed before the peer is instantiated. So
548 # And handshake is performed before the peer is instantiated. So
549 # we need no custom code.
549 # we need no custom code.
550
550
551 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
551 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
552 """Make a peer instance from existing pipes.
552 """Make a peer instance from existing pipes.
553
553
554 ``path`` and ``proc`` are stored on the eventual peer instance and may
554 ``path`` and ``proc`` are stored on the eventual peer instance and may
555 not be used for anything meaningful.
555 not be used for anything meaningful.
556
556
557 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
557 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
558 SSH server's stdio handles.
558 SSH server's stdio handles.
559
559
560 This function is factored out to allow creating peers that don't
560 This function is factored out to allow creating peers that don't
561 actually spawn a new process. It is useful for starting SSH protocol
561 actually spawn a new process. It is useful for starting SSH protocol
562 servers and clients via non-standard means, which can be useful for
562 servers and clients via non-standard means, which can be useful for
563 testing.
563 testing.
564 """
564 """
565 try:
565 try:
566 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
566 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
567 except Exception:
567 except Exception:
568 _cleanuppipes(ui, stdout, stdin, stderr)
568 _cleanuppipes(ui, stdout, stdin, stderr)
569 raise
569 raise
570
570
571 if protoname == wireprototypes.SSHV1:
571 if protoname == wireprototypes.SSHV1:
572 return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps,
572 return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps,
573 autoreadstderr=autoreadstderr)
573 autoreadstderr=autoreadstderr)
574 elif protoname == wireprototypes.SSHV2:
574 elif protoname == wireprototypes.SSHV2:
575 return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps,
575 return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps,
576 autoreadstderr=autoreadstderr)
576 autoreadstderr=autoreadstderr)
577 else:
577 else:
578 _cleanuppipes(ui, stdout, stdin, stderr)
578 _cleanuppipes(ui, stdout, stdin, stderr)
579 raise error.RepoError(_('unknown version of SSH protocol: %s') %
579 raise error.RepoError(_('unknown version of SSH protocol: %s') %
580 protoname)
580 protoname)
581
581
582 def instance(ui, path, create):
582 def instance(ui, path, create):
583 """Create an SSH peer.
583 """Create an SSH peer.
584
584
585 The returned object conforms to the ``wireproto.wirepeer`` interface.
585 The returned object conforms to the ``wireproto.wirepeer`` interface.
586 """
586 """
587 u = util.url(path, parsequery=False, parsefragment=False)
587 u = util.url(path, parsequery=False, parsefragment=False)
588 if u.scheme != 'ssh' or not u.host or u.path is None:
588 if u.scheme != 'ssh' or not u.host or u.path is None:
589 raise error.RepoError(_("couldn't parse location %s") % path)
589 raise error.RepoError(_("couldn't parse location %s") % path)
590
590
591 util.checksafessh(path)
591 util.checksafessh(path)
592
592
593 if u.passwd is not None:
593 if u.passwd is not None:
594 raise error.RepoError(_('password in URL not supported'))
594 raise error.RepoError(_('password in URL not supported'))
595
595
596 sshcmd = ui.config('ui', 'ssh')
596 sshcmd = ui.config('ui', 'ssh')
597 remotecmd = ui.config('ui', 'remotecmd')
597 remotecmd = ui.config('ui', 'remotecmd')
598 sshaddenv = dict(ui.configitems('sshenv'))
598 sshaddenv = dict(ui.configitems('sshenv'))
599 sshenv = procutil.shellenviron(sshaddenv)
599 sshenv = procutil.shellenviron(sshaddenv)
600 remotepath = u.path or '.'
600 remotepath = u.path or '.'
601
601
602 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
602 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
603
603
604 if create:
604 if create:
605 cmd = '%s %s %s' % (sshcmd, args,
605 cmd = '%s %s %s' % (sshcmd, args,
606 procutil.shellquote('%s init %s' %
606 procutil.shellquote('%s init %s' %
607 (_serverquote(remotecmd), _serverquote(remotepath))))
607 (_serverquote(remotecmd), _serverquote(remotepath))))
608 ui.debug('running %s\n' % cmd)
608 ui.debug('running %s\n' % cmd)
609 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
609 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
610 if res != 0:
610 if res != 0:
611 raise error.RepoError(_('could not create remote repo'))
611 raise error.RepoError(_('could not create remote repo'))
612
612
613 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
613 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
614 remotepath, sshenv)
614 remotepath, sshenv)
615
615
616 return makepeer(ui, path, proc, stdin, stdout, stderr)
616 return makepeer(ui, path, proc, stdin, stdout, stderr)
@@ -1,1163 +1,1163 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 # mapping of options accepted by getbundle and their types
162 # mapping of options accepted by getbundle and their types
163 #
163 #
164 # Meant to be extended by extensions. It is extensions responsibility to ensure
164 # Meant to be extended by extensions. It is extensions responsibility to ensure
165 # such options are properly processed in exchange.getbundle.
165 # such options are properly processed in exchange.getbundle.
166 #
166 #
167 # supported types are:
167 # supported types are:
168 #
168 #
169 # :nodes: list of binary nodes
169 # :nodes: list of binary nodes
170 # :csv: list of comma-separated values
170 # :csv: list of comma-separated values
171 # :scsv: list of comma-separated values return as set
171 # :scsv: list of comma-separated values return as set
172 # :plain: string with no transformation needed.
172 # :plain: string with no transformation needed.
173 gboptsmap = {'heads': 'nodes',
173 gboptsmap = {'heads': 'nodes',
174 'bookmarks': 'boolean',
174 'bookmarks': 'boolean',
175 'common': 'nodes',
175 'common': 'nodes',
176 'obsmarkers': 'boolean',
176 'obsmarkers': 'boolean',
177 'phases': 'boolean',
177 'phases': 'boolean',
178 'bundlecaps': 'scsv',
178 'bundlecaps': 'scsv',
179 'listkeys': 'csv',
179 'listkeys': 'csv',
180 'cg': 'boolean',
180 'cg': 'boolean',
181 'cbattempted': 'boolean',
181 'cbattempted': 'boolean',
182 'stream': 'boolean',
182 'stream': 'boolean',
183 }
183 }
184
184
185 # client side
185 # client side
186
186
187 class wirepeer(repository.legacypeer):
187 class wirepeer(repository.legacypeer):
188 """Client-side interface for communicating with a peer repository.
188 """Client-side interface for communicating with a peer repository.
189
189
190 Methods commonly call wire protocol commands of the same name.
190 Methods commonly call wire protocol commands of the same name.
191
191
192 See also httppeer.py and sshpeer.py for protocol-specific
192 See also httppeer.py and sshpeer.py for protocol-specific
193 implementations of this interface.
193 implementations of this interface.
194 """
194 """
195 # Begin of basewirepeer interface.
195 # Begin of ipeercommands interface.
196
196
197 def iterbatch(self):
197 def iterbatch(self):
198 return remoteiterbatcher(self)
198 return remoteiterbatcher(self)
199
199
200 @batchable
200 @batchable
201 def lookup(self, key):
201 def lookup(self, key):
202 self.requirecap('lookup', _('look up remote revision'))
202 self.requirecap('lookup', _('look up remote revision'))
203 f = future()
203 f = future()
204 yield {'key': encoding.fromlocal(key)}, f
204 yield {'key': encoding.fromlocal(key)}, f
205 d = f.value
205 d = f.value
206 success, data = d[:-1].split(" ", 1)
206 success, data = d[:-1].split(" ", 1)
207 if int(success):
207 if int(success):
208 yield bin(data)
208 yield bin(data)
209 else:
209 else:
210 self._abort(error.RepoError(data))
210 self._abort(error.RepoError(data))
211
211
212 @batchable
212 @batchable
213 def heads(self):
213 def heads(self):
214 f = future()
214 f = future()
215 yield {}, f
215 yield {}, f
216 d = f.value
216 d = f.value
217 try:
217 try:
218 yield decodelist(d[:-1])
218 yield decodelist(d[:-1])
219 except ValueError:
219 except ValueError:
220 self._abort(error.ResponseError(_("unexpected response:"), d))
220 self._abort(error.ResponseError(_("unexpected response:"), d))
221
221
222 @batchable
222 @batchable
223 def known(self, nodes):
223 def known(self, nodes):
224 f = future()
224 f = future()
225 yield {'nodes': encodelist(nodes)}, f
225 yield {'nodes': encodelist(nodes)}, f
226 d = f.value
226 d = f.value
227 try:
227 try:
228 yield [bool(int(b)) for b in d]
228 yield [bool(int(b)) for b in d]
229 except ValueError:
229 except ValueError:
230 self._abort(error.ResponseError(_("unexpected response:"), d))
230 self._abort(error.ResponseError(_("unexpected response:"), d))
231
231
232 @batchable
232 @batchable
233 def branchmap(self):
233 def branchmap(self):
234 f = future()
234 f = future()
235 yield {}, f
235 yield {}, f
236 d = f.value
236 d = f.value
237 try:
237 try:
238 branchmap = {}
238 branchmap = {}
239 for branchpart in d.splitlines():
239 for branchpart in d.splitlines():
240 branchname, branchheads = branchpart.split(' ', 1)
240 branchname, branchheads = branchpart.split(' ', 1)
241 branchname = encoding.tolocal(urlreq.unquote(branchname))
241 branchname = encoding.tolocal(urlreq.unquote(branchname))
242 branchheads = decodelist(branchheads)
242 branchheads = decodelist(branchheads)
243 branchmap[branchname] = branchheads
243 branchmap[branchname] = branchheads
244 yield branchmap
244 yield branchmap
245 except TypeError:
245 except TypeError:
246 self._abort(error.ResponseError(_("unexpected response:"), d))
246 self._abort(error.ResponseError(_("unexpected response:"), d))
247
247
248 @batchable
248 @batchable
249 def listkeys(self, namespace):
249 def listkeys(self, namespace):
250 if not self.capable('pushkey'):
250 if not self.capable('pushkey'):
251 yield {}, None
251 yield {}, None
252 f = future()
252 f = future()
253 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
253 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
254 yield {'namespace': encoding.fromlocal(namespace)}, f
254 yield {'namespace': encoding.fromlocal(namespace)}, f
255 d = f.value
255 d = f.value
256 self.ui.debug('received listkey for "%s": %i bytes\n'
256 self.ui.debug('received listkey for "%s": %i bytes\n'
257 % (namespace, len(d)))
257 % (namespace, len(d)))
258 yield pushkeymod.decodekeys(d)
258 yield pushkeymod.decodekeys(d)
259
259
260 @batchable
260 @batchable
261 def pushkey(self, namespace, key, old, new):
261 def pushkey(self, namespace, key, old, new):
262 if not self.capable('pushkey'):
262 if not self.capable('pushkey'):
263 yield False, None
263 yield False, None
264 f = future()
264 f = future()
265 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
265 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
266 yield {'namespace': encoding.fromlocal(namespace),
266 yield {'namespace': encoding.fromlocal(namespace),
267 'key': encoding.fromlocal(key),
267 'key': encoding.fromlocal(key),
268 'old': encoding.fromlocal(old),
268 'old': encoding.fromlocal(old),
269 'new': encoding.fromlocal(new)}, f
269 'new': encoding.fromlocal(new)}, f
270 d = f.value
270 d = f.value
271 d, output = d.split('\n', 1)
271 d, output = d.split('\n', 1)
272 try:
272 try:
273 d = bool(int(d))
273 d = bool(int(d))
274 except ValueError:
274 except ValueError:
275 raise error.ResponseError(
275 raise error.ResponseError(
276 _('push failed (unexpected response):'), d)
276 _('push failed (unexpected response):'), d)
277 for l in output.splitlines(True):
277 for l in output.splitlines(True):
278 self.ui.status(_('remote: '), l)
278 self.ui.status(_('remote: '), l)
279 yield d
279 yield d
280
280
281 def stream_out(self):
281 def stream_out(self):
282 return self._callstream('stream_out')
282 return self._callstream('stream_out')
283
283
284 def getbundle(self, source, **kwargs):
284 def getbundle(self, source, **kwargs):
285 kwargs = pycompat.byteskwargs(kwargs)
285 kwargs = pycompat.byteskwargs(kwargs)
286 self.requirecap('getbundle', _('look up remote changes'))
286 self.requirecap('getbundle', _('look up remote changes'))
287 opts = {}
287 opts = {}
288 bundlecaps = kwargs.get('bundlecaps')
288 bundlecaps = kwargs.get('bundlecaps')
289 if bundlecaps is not None:
289 if bundlecaps is not None:
290 kwargs['bundlecaps'] = sorted(bundlecaps)
290 kwargs['bundlecaps'] = sorted(bundlecaps)
291 else:
291 else:
292 bundlecaps = () # kwargs could have it to None
292 bundlecaps = () # kwargs could have it to None
293 for key, value in kwargs.iteritems():
293 for key, value in kwargs.iteritems():
294 if value is None:
294 if value is None:
295 continue
295 continue
296 keytype = gboptsmap.get(key)
296 keytype = gboptsmap.get(key)
297 if keytype is None:
297 if keytype is None:
298 raise error.ProgrammingError(
298 raise error.ProgrammingError(
299 'Unexpectedly None keytype for key %s' % key)
299 'Unexpectedly None keytype for key %s' % key)
300 elif keytype == 'nodes':
300 elif keytype == 'nodes':
301 value = encodelist(value)
301 value = encodelist(value)
302 elif keytype in ('csv', 'scsv'):
302 elif keytype in ('csv', 'scsv'):
303 value = ','.join(value)
303 value = ','.join(value)
304 elif keytype == 'boolean':
304 elif keytype == 'boolean':
305 value = '%i' % bool(value)
305 value = '%i' % bool(value)
306 elif keytype != 'plain':
306 elif keytype != 'plain':
307 raise KeyError('unknown getbundle option type %s'
307 raise KeyError('unknown getbundle option type %s'
308 % keytype)
308 % keytype)
309 opts[key] = value
309 opts[key] = value
310 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
310 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
311 if any((cap.startswith('HG2') for cap in bundlecaps)):
311 if any((cap.startswith('HG2') for cap in bundlecaps)):
312 return bundle2.getunbundler(self.ui, f)
312 return bundle2.getunbundler(self.ui, f)
313 else:
313 else:
314 return changegroupmod.cg1unpacker(f, 'UN')
314 return changegroupmod.cg1unpacker(f, 'UN')
315
315
316 def unbundle(self, cg, heads, url):
316 def unbundle(self, cg, heads, url):
317 '''Send cg (a readable file-like object representing the
317 '''Send cg (a readable file-like object representing the
318 changegroup to push, typically a chunkbuffer object) to the
318 changegroup to push, typically a chunkbuffer object) to the
319 remote server as a bundle.
319 remote server as a bundle.
320
320
321 When pushing a bundle10 stream, return an integer indicating the
321 When pushing a bundle10 stream, return an integer indicating the
322 result of the push (see changegroup.apply()).
322 result of the push (see changegroup.apply()).
323
323
324 When pushing a bundle20 stream, return a bundle20 stream.
324 When pushing a bundle20 stream, return a bundle20 stream.
325
325
326 `url` is the url the client thinks it's pushing to, which is
326 `url` is the url the client thinks it's pushing to, which is
327 visible to hooks.
327 visible to hooks.
328 '''
328 '''
329
329
330 if heads != ['force'] and self.capable('unbundlehash'):
330 if heads != ['force'] and self.capable('unbundlehash'):
331 heads = encodelist(['hashed',
331 heads = encodelist(['hashed',
332 hashlib.sha1(''.join(sorted(heads))).digest()])
332 hashlib.sha1(''.join(sorted(heads))).digest()])
333 else:
333 else:
334 heads = encodelist(heads)
334 heads = encodelist(heads)
335
335
336 if util.safehasattr(cg, 'deltaheader'):
336 if util.safehasattr(cg, 'deltaheader'):
337 # this a bundle10, do the old style call sequence
337 # this a bundle10, do the old style call sequence
338 ret, output = self._callpush("unbundle", cg, heads=heads)
338 ret, output = self._callpush("unbundle", cg, heads=heads)
339 if ret == "":
339 if ret == "":
340 raise error.ResponseError(
340 raise error.ResponseError(
341 _('push failed:'), output)
341 _('push failed:'), output)
342 try:
342 try:
343 ret = int(ret)
343 ret = int(ret)
344 except ValueError:
344 except ValueError:
345 raise error.ResponseError(
345 raise error.ResponseError(
346 _('push failed (unexpected response):'), ret)
346 _('push failed (unexpected response):'), ret)
347
347
348 for l in output.splitlines(True):
348 for l in output.splitlines(True):
349 self.ui.status(_('remote: '), l)
349 self.ui.status(_('remote: '), l)
350 else:
350 else:
351 # bundle2 push. Send a stream, fetch a stream.
351 # bundle2 push. Send a stream, fetch a stream.
352 stream = self._calltwowaystream('unbundle', cg, heads=heads)
352 stream = self._calltwowaystream('unbundle', cg, heads=heads)
353 ret = bundle2.getunbundler(self.ui, stream)
353 ret = bundle2.getunbundler(self.ui, stream)
354 return ret
354 return ret
355
355
356 # End of basewirepeer interface.
356 # End of ipeercommands interface.
357
357
358 # Begin of baselegacywirepeer interface.
358 # Begin of ipeerlegacycommands interface.
359
359
360 def branches(self, nodes):
360 def branches(self, nodes):
361 n = encodelist(nodes)
361 n = encodelist(nodes)
362 d = self._call("branches", nodes=n)
362 d = self._call("branches", nodes=n)
363 try:
363 try:
364 br = [tuple(decodelist(b)) for b in d.splitlines()]
364 br = [tuple(decodelist(b)) for b in d.splitlines()]
365 return br
365 return br
366 except ValueError:
366 except ValueError:
367 self._abort(error.ResponseError(_("unexpected response:"), d))
367 self._abort(error.ResponseError(_("unexpected response:"), d))
368
368
369 def between(self, pairs):
369 def between(self, pairs):
370 batch = 8 # avoid giant requests
370 batch = 8 # avoid giant requests
371 r = []
371 r = []
372 for i in xrange(0, len(pairs), batch):
372 for i in xrange(0, len(pairs), batch):
373 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
373 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
374 d = self._call("between", pairs=n)
374 d = self._call("between", pairs=n)
375 try:
375 try:
376 r.extend(l and decodelist(l) or [] for l in d.splitlines())
376 r.extend(l and decodelist(l) or [] for l in d.splitlines())
377 except ValueError:
377 except ValueError:
378 self._abort(error.ResponseError(_("unexpected response:"), d))
378 self._abort(error.ResponseError(_("unexpected response:"), d))
379 return r
379 return r
380
380
381 def changegroup(self, nodes, kind):
381 def changegroup(self, nodes, kind):
382 n = encodelist(nodes)
382 n = encodelist(nodes)
383 f = self._callcompressable("changegroup", roots=n)
383 f = self._callcompressable("changegroup", roots=n)
384 return changegroupmod.cg1unpacker(f, 'UN')
384 return changegroupmod.cg1unpacker(f, 'UN')
385
385
386 def changegroupsubset(self, bases, heads, kind):
386 def changegroupsubset(self, bases, heads, kind):
387 self.requirecap('changegroupsubset', _('look up remote changes'))
387 self.requirecap('changegroupsubset', _('look up remote changes'))
388 bases = encodelist(bases)
388 bases = encodelist(bases)
389 heads = encodelist(heads)
389 heads = encodelist(heads)
390 f = self._callcompressable("changegroupsubset",
390 f = self._callcompressable("changegroupsubset",
391 bases=bases, heads=heads)
391 bases=bases, heads=heads)
392 return changegroupmod.cg1unpacker(f, 'UN')
392 return changegroupmod.cg1unpacker(f, 'UN')
393
393
394 # End of baselegacywirepeer interface.
394 # End of ipeerlegacycommands interface.
395
395
396 def _submitbatch(self, req):
396 def _submitbatch(self, req):
397 """run batch request <req> on the server
397 """run batch request <req> on the server
398
398
399 Returns an iterator of the raw responses from the server.
399 Returns an iterator of the raw responses from the server.
400 """
400 """
401 ui = self.ui
401 ui = self.ui
402 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
402 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
403 ui.debug('devel-peer-request: batched-content\n')
403 ui.debug('devel-peer-request: batched-content\n')
404 for op, args in req:
404 for op, args in req:
405 msg = 'devel-peer-request: - %s (%d arguments)\n'
405 msg = 'devel-peer-request: - %s (%d arguments)\n'
406 ui.debug(msg % (op, len(args)))
406 ui.debug(msg % (op, len(args)))
407
407
408 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
408 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
409 chunk = rsp.read(1024)
409 chunk = rsp.read(1024)
410 work = [chunk]
410 work = [chunk]
411 while chunk:
411 while chunk:
412 while ';' not in chunk and chunk:
412 while ';' not in chunk and chunk:
413 chunk = rsp.read(1024)
413 chunk = rsp.read(1024)
414 work.append(chunk)
414 work.append(chunk)
415 merged = ''.join(work)
415 merged = ''.join(work)
416 while ';' in merged:
416 while ';' in merged:
417 one, merged = merged.split(';', 1)
417 one, merged = merged.split(';', 1)
418 yield unescapearg(one)
418 yield unescapearg(one)
419 chunk = rsp.read(1024)
419 chunk = rsp.read(1024)
420 work = [merged, chunk]
420 work = [merged, chunk]
421 yield unescapearg(''.join(work))
421 yield unescapearg(''.join(work))
422
422
423 def _submitone(self, op, args):
423 def _submitone(self, op, args):
424 return self._call(op, **pycompat.strkwargs(args))
424 return self._call(op, **pycompat.strkwargs(args))
425
425
426 def debugwireargs(self, one, two, three=None, four=None, five=None):
426 def debugwireargs(self, one, two, three=None, four=None, five=None):
427 # don't pass optional arguments left at their default value
427 # don't pass optional arguments left at their default value
428 opts = {}
428 opts = {}
429 if three is not None:
429 if three is not None:
430 opts[r'three'] = three
430 opts[r'three'] = three
431 if four is not None:
431 if four is not None:
432 opts[r'four'] = four
432 opts[r'four'] = four
433 return self._call('debugwireargs', one=one, two=two, **opts)
433 return self._call('debugwireargs', one=one, two=two, **opts)
434
434
435 def _call(self, cmd, **args):
435 def _call(self, cmd, **args):
436 """execute <cmd> on the server
436 """execute <cmd> on the server
437
437
438 The command is expected to return a simple string.
438 The command is expected to return a simple string.
439
439
440 returns the server reply as a string."""
440 returns the server reply as a string."""
441 raise NotImplementedError()
441 raise NotImplementedError()
442
442
443 def _callstream(self, cmd, **args):
443 def _callstream(self, cmd, **args):
444 """execute <cmd> on the server
444 """execute <cmd> on the server
445
445
446 The command is expected to return a stream. Note that if the
446 The command is expected to return a stream. Note that if the
447 command doesn't return a stream, _callstream behaves
447 command doesn't return a stream, _callstream behaves
448 differently for ssh and http peers.
448 differently for ssh and http peers.
449
449
450 returns the server reply as a file like object.
450 returns the server reply as a file like object.
451 """
451 """
452 raise NotImplementedError()
452 raise NotImplementedError()
453
453
454 def _callcompressable(self, cmd, **args):
454 def _callcompressable(self, cmd, **args):
455 """execute <cmd> on the server
455 """execute <cmd> on the server
456
456
457 The command is expected to return a stream.
457 The command is expected to return a stream.
458
458
459 The stream may have been compressed in some implementations. This
459 The stream may have been compressed in some implementations. This
460 function takes care of the decompression. This is the only difference
460 function takes care of the decompression. This is the only difference
461 with _callstream.
461 with _callstream.
462
462
463 returns the server reply as a file like object.
463 returns the server reply as a file like object.
464 """
464 """
465 raise NotImplementedError()
465 raise NotImplementedError()
466
466
467 def _callpush(self, cmd, fp, **args):
467 def _callpush(self, cmd, fp, **args):
468 """execute a <cmd> on server
468 """execute a <cmd> on server
469
469
470 The command is expected to be related to a push. Push has a special
470 The command is expected to be related to a push. Push has a special
471 return method.
471 return method.
472
472
473 returns the server reply as a (ret, output) tuple. ret is either
473 returns the server reply as a (ret, output) tuple. ret is either
474 empty (error) or a stringified int.
474 empty (error) or a stringified int.
475 """
475 """
476 raise NotImplementedError()
476 raise NotImplementedError()
477
477
478 def _calltwowaystream(self, cmd, fp, **args):
478 def _calltwowaystream(self, cmd, fp, **args):
479 """execute <cmd> on server
479 """execute <cmd> on server
480
480
481 The command will send a stream to the server and get a stream in reply.
481 The command will send a stream to the server and get a stream in reply.
482 """
482 """
483 raise NotImplementedError()
483 raise NotImplementedError()
484
484
485 def _abort(self, exception):
485 def _abort(self, exception):
486 """clearly abort the wire protocol connection and raise the exception
486 """clearly abort the wire protocol connection and raise the exception
487 """
487 """
488 raise NotImplementedError()
488 raise NotImplementedError()
489
489
490 # server side
490 # server side
491
491
492 # wire protocol command can either return a string or one of these classes.
492 # wire protocol command can either return a string or one of these classes.
493
493
494 def getdispatchrepo(repo, proto, command):
494 def getdispatchrepo(repo, proto, command):
495 """Obtain the repo used for processing wire protocol commands.
495 """Obtain the repo used for processing wire protocol commands.
496
496
497 The intent of this function is to serve as a monkeypatch point for
497 The intent of this function is to serve as a monkeypatch point for
498 extensions that need commands to operate on different repo views under
498 extensions that need commands to operate on different repo views under
499 specialized circumstances.
499 specialized circumstances.
500 """
500 """
501 return repo.filtered('served')
501 return repo.filtered('served')
502
502
503 def dispatch(repo, proto, command):
503 def dispatch(repo, proto, command):
504 repo = getdispatchrepo(repo, proto, command)
504 repo = getdispatchrepo(repo, proto, command)
505
505
506 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
506 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
507 commandtable = commandsv2 if transportversion == 2 else commands
507 commandtable = commandsv2 if transportversion == 2 else commands
508 func, spec = commandtable[command]
508 func, spec = commandtable[command]
509
509
510 args = proto.getargs(spec)
510 args = proto.getargs(spec)
511 return func(repo, proto, *args)
511 return func(repo, proto, *args)
512
512
513 def options(cmd, keys, others):
513 def options(cmd, keys, others):
514 opts = {}
514 opts = {}
515 for k in keys:
515 for k in keys:
516 if k in others:
516 if k in others:
517 opts[k] = others[k]
517 opts[k] = others[k]
518 del others[k]
518 del others[k]
519 if others:
519 if others:
520 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
520 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
521 % (cmd, ",".join(others)))
521 % (cmd, ",".join(others)))
522 return opts
522 return opts
523
523
524 def bundle1allowed(repo, action):
524 def bundle1allowed(repo, action):
525 """Whether a bundle1 operation is allowed from the server.
525 """Whether a bundle1 operation is allowed from the server.
526
526
527 Priority is:
527 Priority is:
528
528
529 1. server.bundle1gd.<action> (if generaldelta active)
529 1. server.bundle1gd.<action> (if generaldelta active)
530 2. server.bundle1.<action>
530 2. server.bundle1.<action>
531 3. server.bundle1gd (if generaldelta active)
531 3. server.bundle1gd (if generaldelta active)
532 4. server.bundle1
532 4. server.bundle1
533 """
533 """
534 ui = repo.ui
534 ui = repo.ui
535 gd = 'generaldelta' in repo.requirements
535 gd = 'generaldelta' in repo.requirements
536
536
537 if gd:
537 if gd:
538 v = ui.configbool('server', 'bundle1gd.%s' % action)
538 v = ui.configbool('server', 'bundle1gd.%s' % action)
539 if v is not None:
539 if v is not None:
540 return v
540 return v
541
541
542 v = ui.configbool('server', 'bundle1.%s' % action)
542 v = ui.configbool('server', 'bundle1.%s' % action)
543 if v is not None:
543 if v is not None:
544 return v
544 return v
545
545
546 if gd:
546 if gd:
547 v = ui.configbool('server', 'bundle1gd')
547 v = ui.configbool('server', 'bundle1gd')
548 if v is not None:
548 if v is not None:
549 return v
549 return v
550
550
551 return ui.configbool('server', 'bundle1')
551 return ui.configbool('server', 'bundle1')
552
552
553 def supportedcompengines(ui, role):
553 def supportedcompengines(ui, role):
554 """Obtain the list of supported compression engines for a request."""
554 """Obtain the list of supported compression engines for a request."""
555 assert role in (util.CLIENTROLE, util.SERVERROLE)
555 assert role in (util.CLIENTROLE, util.SERVERROLE)
556
556
557 compengines = util.compengines.supportedwireengines(role)
557 compengines = util.compengines.supportedwireengines(role)
558
558
559 # Allow config to override default list and ordering.
559 # Allow config to override default list and ordering.
560 if role == util.SERVERROLE:
560 if role == util.SERVERROLE:
561 configengines = ui.configlist('server', 'compressionengines')
561 configengines = ui.configlist('server', 'compressionengines')
562 config = 'server.compressionengines'
562 config = 'server.compressionengines'
563 else:
563 else:
564 # This is currently implemented mainly to facilitate testing. In most
564 # This is currently implemented mainly to facilitate testing. In most
565 # cases, the server should be in charge of choosing a compression engine
565 # cases, the server should be in charge of choosing a compression engine
566 # because a server has the most to lose from a sub-optimal choice. (e.g.
566 # because a server has the most to lose from a sub-optimal choice. (e.g.
567 # CPU DoS due to an expensive engine or a network DoS due to poor
567 # CPU DoS due to an expensive engine or a network DoS due to poor
568 # compression ratio).
568 # compression ratio).
569 configengines = ui.configlist('experimental',
569 configengines = ui.configlist('experimental',
570 'clientcompressionengines')
570 'clientcompressionengines')
571 config = 'experimental.clientcompressionengines'
571 config = 'experimental.clientcompressionengines'
572
572
573 # No explicit config. Filter out the ones that aren't supposed to be
573 # No explicit config. Filter out the ones that aren't supposed to be
574 # advertised and return default ordering.
574 # advertised and return default ordering.
575 if not configengines:
575 if not configengines:
576 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
576 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
577 return [e for e in compengines
577 return [e for e in compengines
578 if getattr(e.wireprotosupport(), attr) > 0]
578 if getattr(e.wireprotosupport(), attr) > 0]
579
579
580 # If compression engines are listed in the config, assume there is a good
580 # If compression engines are listed in the config, assume there is a good
581 # reason for it (like server operators wanting to achieve specific
581 # reason for it (like server operators wanting to achieve specific
582 # performance characteristics). So fail fast if the config references
582 # performance characteristics). So fail fast if the config references
583 # unusable compression engines.
583 # unusable compression engines.
584 validnames = set(e.name() for e in compengines)
584 validnames = set(e.name() for e in compengines)
585 invalidnames = set(e for e in configengines if e not in validnames)
585 invalidnames = set(e for e in configengines if e not in validnames)
586 if invalidnames:
586 if invalidnames:
587 raise error.Abort(_('invalid compression engine defined in %s: %s') %
587 raise error.Abort(_('invalid compression engine defined in %s: %s') %
588 (config, ', '.join(sorted(invalidnames))))
588 (config, ', '.join(sorted(invalidnames))))
589
589
590 compengines = [e for e in compengines if e.name() in configengines]
590 compengines = [e for e in compengines if e.name() in configengines]
591 compengines = sorted(compengines,
591 compengines = sorted(compengines,
592 key=lambda e: configengines.index(e.name()))
592 key=lambda e: configengines.index(e.name()))
593
593
594 if not compengines:
594 if not compengines:
595 raise error.Abort(_('%s config option does not specify any known '
595 raise error.Abort(_('%s config option does not specify any known '
596 'compression engines') % config,
596 'compression engines') % config,
597 hint=_('usable compression engines: %s') %
597 hint=_('usable compression engines: %s') %
598 ', '.sorted(validnames))
598 ', '.sorted(validnames))
599
599
600 return compengines
600 return compengines
601
601
602 class commandentry(object):
602 class commandentry(object):
603 """Represents a declared wire protocol command."""
603 """Represents a declared wire protocol command."""
604 def __init__(self, func, args='', transports=None,
604 def __init__(self, func, args='', transports=None,
605 permission='push'):
605 permission='push'):
606 self.func = func
606 self.func = func
607 self.args = args
607 self.args = args
608 self.transports = transports or set()
608 self.transports = transports or set()
609 self.permission = permission
609 self.permission = permission
610
610
611 def _merge(self, func, args):
611 def _merge(self, func, args):
612 """Merge this instance with an incoming 2-tuple.
612 """Merge this instance with an incoming 2-tuple.
613
613
614 This is called when a caller using the old 2-tuple API attempts
614 This is called when a caller using the old 2-tuple API attempts
615 to replace an instance. The incoming values are merged with
615 to replace an instance. The incoming values are merged with
616 data not captured by the 2-tuple and a new instance containing
616 data not captured by the 2-tuple and a new instance containing
617 the union of the two objects is returned.
617 the union of the two objects is returned.
618 """
618 """
619 return commandentry(func, args=args, transports=set(self.transports),
619 return commandentry(func, args=args, transports=set(self.transports),
620 permission=self.permission)
620 permission=self.permission)
621
621
622 # Old code treats instances as 2-tuples. So expose that interface.
622 # Old code treats instances as 2-tuples. So expose that interface.
623 def __iter__(self):
623 def __iter__(self):
624 yield self.func
624 yield self.func
625 yield self.args
625 yield self.args
626
626
627 def __getitem__(self, i):
627 def __getitem__(self, i):
628 if i == 0:
628 if i == 0:
629 return self.func
629 return self.func
630 elif i == 1:
630 elif i == 1:
631 return self.args
631 return self.args
632 else:
632 else:
633 raise IndexError('can only access elements 0 and 1')
633 raise IndexError('can only access elements 0 and 1')
634
634
635 class commanddict(dict):
635 class commanddict(dict):
636 """Container for registered wire protocol commands.
636 """Container for registered wire protocol commands.
637
637
638 It behaves like a dict. But __setitem__ is overwritten to allow silent
638 It behaves like a dict. But __setitem__ is overwritten to allow silent
639 coercion of values from 2-tuples for API compatibility.
639 coercion of values from 2-tuples for API compatibility.
640 """
640 """
641 def __setitem__(self, k, v):
641 def __setitem__(self, k, v):
642 if isinstance(v, commandentry):
642 if isinstance(v, commandentry):
643 pass
643 pass
644 # Cast 2-tuples to commandentry instances.
644 # Cast 2-tuples to commandentry instances.
645 elif isinstance(v, tuple):
645 elif isinstance(v, tuple):
646 if len(v) != 2:
646 if len(v) != 2:
647 raise ValueError('command tuples must have exactly 2 elements')
647 raise ValueError('command tuples must have exactly 2 elements')
648
648
649 # It is common for extensions to wrap wire protocol commands via
649 # It is common for extensions to wrap wire protocol commands via
650 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
650 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
651 # doing this aren't aware of the new API that uses objects to store
651 # doing this aren't aware of the new API that uses objects to store
652 # command entries, we automatically merge old state with new.
652 # command entries, we automatically merge old state with new.
653 if k in self:
653 if k in self:
654 v = self[k]._merge(v[0], v[1])
654 v = self[k]._merge(v[0], v[1])
655 else:
655 else:
656 # Use default values from @wireprotocommand.
656 # Use default values from @wireprotocommand.
657 v = commandentry(v[0], args=v[1],
657 v = commandentry(v[0], args=v[1],
658 transports=set(wireprototypes.TRANSPORTS),
658 transports=set(wireprototypes.TRANSPORTS),
659 permission='push')
659 permission='push')
660 else:
660 else:
661 raise ValueError('command entries must be commandentry instances '
661 raise ValueError('command entries must be commandentry instances '
662 'or 2-tuples')
662 'or 2-tuples')
663
663
664 return super(commanddict, self).__setitem__(k, v)
664 return super(commanddict, self).__setitem__(k, v)
665
665
666 def commandavailable(self, command, proto):
666 def commandavailable(self, command, proto):
667 """Determine if a command is available for the requested protocol."""
667 """Determine if a command is available for the requested protocol."""
668 assert proto.name in wireprototypes.TRANSPORTS
668 assert proto.name in wireprototypes.TRANSPORTS
669
669
670 entry = self.get(command)
670 entry = self.get(command)
671
671
672 if not entry:
672 if not entry:
673 return False
673 return False
674
674
675 if proto.name not in entry.transports:
675 if proto.name not in entry.transports:
676 return False
676 return False
677
677
678 return True
678 return True
679
679
680 # Constants specifying which transports a wire protocol command should be
680 # Constants specifying which transports a wire protocol command should be
681 # available on. For use with @wireprotocommand.
681 # available on. For use with @wireprotocommand.
682 POLICY_ALL = 'all'
682 POLICY_ALL = 'all'
683 POLICY_V1_ONLY = 'v1-only'
683 POLICY_V1_ONLY = 'v1-only'
684 POLICY_V2_ONLY = 'v2-only'
684 POLICY_V2_ONLY = 'v2-only'
685
685
686 # For version 1 transports.
686 # For version 1 transports.
687 commands = commanddict()
687 commands = commanddict()
688
688
689 # For version 2 transports.
689 # For version 2 transports.
690 commandsv2 = commanddict()
690 commandsv2 = commanddict()
691
691
692 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
692 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL,
693 permission='push'):
693 permission='push'):
694 """Decorator to declare a wire protocol command.
694 """Decorator to declare a wire protocol command.
695
695
696 ``name`` is the name of the wire protocol command being provided.
696 ``name`` is the name of the wire protocol command being provided.
697
697
698 ``args`` is a space-delimited list of named arguments that the command
698 ``args`` is a space-delimited list of named arguments that the command
699 accepts. ``*`` is a special value that says to accept all arguments.
699 accepts. ``*`` is a special value that says to accept all arguments.
700
700
701 ``transportpolicy`` is a POLICY_* constant denoting which transports
701 ``transportpolicy`` is a POLICY_* constant denoting which transports
702 this wire protocol command should be exposed to. By default, commands
702 this wire protocol command should be exposed to. By default, commands
703 are exposed to all wire protocol transports.
703 are exposed to all wire protocol transports.
704
704
705 ``permission`` defines the permission type needed to run this command.
705 ``permission`` defines the permission type needed to run this command.
706 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
706 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
707 respectively. Default is to assume command requires ``push`` permissions
707 respectively. Default is to assume command requires ``push`` permissions
708 because otherwise commands not declaring their permissions could modify
708 because otherwise commands not declaring their permissions could modify
709 a repository that is supposed to be read-only.
709 a repository that is supposed to be read-only.
710 """
710 """
711 if transportpolicy == POLICY_ALL:
711 if transportpolicy == POLICY_ALL:
712 transports = set(wireprototypes.TRANSPORTS)
712 transports = set(wireprototypes.TRANSPORTS)
713 transportversions = {1, 2}
713 transportversions = {1, 2}
714 elif transportpolicy == POLICY_V1_ONLY:
714 elif transportpolicy == POLICY_V1_ONLY:
715 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
715 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
716 if v['version'] == 1}
716 if v['version'] == 1}
717 transportversions = {1}
717 transportversions = {1}
718 elif transportpolicy == POLICY_V2_ONLY:
718 elif transportpolicy == POLICY_V2_ONLY:
719 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
719 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
720 if v['version'] == 2}
720 if v['version'] == 2}
721 transportversions = {2}
721 transportversions = {2}
722 else:
722 else:
723 raise error.ProgrammingError('invalid transport policy value: %s' %
723 raise error.ProgrammingError('invalid transport policy value: %s' %
724 transportpolicy)
724 transportpolicy)
725
725
726 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
726 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
727 # SSHv2.
727 # SSHv2.
728 # TODO undo this hack when SSH is using the unified frame protocol.
728 # TODO undo this hack when SSH is using the unified frame protocol.
729 if name == b'batch':
729 if name == b'batch':
730 transports.add(wireprototypes.SSHV2)
730 transports.add(wireprototypes.SSHV2)
731
731
732 if permission not in ('push', 'pull'):
732 if permission not in ('push', 'pull'):
733 raise error.ProgrammingError('invalid wire protocol permission; '
733 raise error.ProgrammingError('invalid wire protocol permission; '
734 'got %s; expected "push" or "pull"' %
734 'got %s; expected "push" or "pull"' %
735 permission)
735 permission)
736
736
737 def register(func):
737 def register(func):
738 if 1 in transportversions:
738 if 1 in transportversions:
739 if name in commands:
739 if name in commands:
740 raise error.ProgrammingError('%s command already registered '
740 raise error.ProgrammingError('%s command already registered '
741 'for version 1' % name)
741 'for version 1' % name)
742 commands[name] = commandentry(func, args=args,
742 commands[name] = commandentry(func, args=args,
743 transports=transports,
743 transports=transports,
744 permission=permission)
744 permission=permission)
745 if 2 in transportversions:
745 if 2 in transportversions:
746 if name in commandsv2:
746 if name in commandsv2:
747 raise error.ProgrammingError('%s command already registered '
747 raise error.ProgrammingError('%s command already registered '
748 'for version 2' % name)
748 'for version 2' % name)
749 commandsv2[name] = commandentry(func, args=args,
749 commandsv2[name] = commandentry(func, args=args,
750 transports=transports,
750 transports=transports,
751 permission=permission)
751 permission=permission)
752
752
753 return func
753 return func
754 return register
754 return register
755
755
756 # TODO define a more appropriate permissions type to use for this.
756 # TODO define a more appropriate permissions type to use for this.
757 @wireprotocommand('batch', 'cmds *', permission='pull',
757 @wireprotocommand('batch', 'cmds *', permission='pull',
758 transportpolicy=POLICY_V1_ONLY)
758 transportpolicy=POLICY_V1_ONLY)
759 def batch(repo, proto, cmds, others):
759 def batch(repo, proto, cmds, others):
760 repo = repo.filtered("served")
760 repo = repo.filtered("served")
761 res = []
761 res = []
762 for pair in cmds.split(';'):
762 for pair in cmds.split(';'):
763 op, args = pair.split(' ', 1)
763 op, args = pair.split(' ', 1)
764 vals = {}
764 vals = {}
765 for a in args.split(','):
765 for a in args.split(','):
766 if a:
766 if a:
767 n, v = a.split('=')
767 n, v = a.split('=')
768 vals[unescapearg(n)] = unescapearg(v)
768 vals[unescapearg(n)] = unescapearg(v)
769 func, spec = commands[op]
769 func, spec = commands[op]
770
770
771 # Validate that client has permissions to perform this command.
771 # Validate that client has permissions to perform this command.
772 perm = commands[op].permission
772 perm = commands[op].permission
773 assert perm in ('push', 'pull')
773 assert perm in ('push', 'pull')
774 proto.checkperm(perm)
774 proto.checkperm(perm)
775
775
776 if spec:
776 if spec:
777 keys = spec.split()
777 keys = spec.split()
778 data = {}
778 data = {}
779 for k in keys:
779 for k in keys:
780 if k == '*':
780 if k == '*':
781 star = {}
781 star = {}
782 for key in vals.keys():
782 for key in vals.keys():
783 if key not in keys:
783 if key not in keys:
784 star[key] = vals[key]
784 star[key] = vals[key]
785 data['*'] = star
785 data['*'] = star
786 else:
786 else:
787 data[k] = vals[k]
787 data[k] = vals[k]
788 result = func(repo, proto, *[data[k] for k in keys])
788 result = func(repo, proto, *[data[k] for k in keys])
789 else:
789 else:
790 result = func(repo, proto)
790 result = func(repo, proto)
791 if isinstance(result, wireprototypes.ooberror):
791 if isinstance(result, wireprototypes.ooberror):
792 return result
792 return result
793
793
794 # For now, all batchable commands must return bytesresponse or
794 # For now, all batchable commands must return bytesresponse or
795 # raw bytes (for backwards compatibility).
795 # raw bytes (for backwards compatibility).
796 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
796 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
797 if isinstance(result, wireprototypes.bytesresponse):
797 if isinstance(result, wireprototypes.bytesresponse):
798 result = result.data
798 result = result.data
799 res.append(escapearg(result))
799 res.append(escapearg(result))
800
800
801 return wireprototypes.bytesresponse(';'.join(res))
801 return wireprototypes.bytesresponse(';'.join(res))
802
802
803 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
803 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY,
804 permission='pull')
804 permission='pull')
805 def between(repo, proto, pairs):
805 def between(repo, proto, pairs):
806 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
806 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
807 r = []
807 r = []
808 for b in repo.between(pairs):
808 for b in repo.between(pairs):
809 r.append(encodelist(b) + "\n")
809 r.append(encodelist(b) + "\n")
810
810
811 return wireprototypes.bytesresponse(''.join(r))
811 return wireprototypes.bytesresponse(''.join(r))
812
812
813 @wireprotocommand('branchmap', permission='pull')
813 @wireprotocommand('branchmap', permission='pull')
814 def branchmap(repo, proto):
814 def branchmap(repo, proto):
815 branchmap = repo.branchmap()
815 branchmap = repo.branchmap()
816 heads = []
816 heads = []
817 for branch, nodes in branchmap.iteritems():
817 for branch, nodes in branchmap.iteritems():
818 branchname = urlreq.quote(encoding.fromlocal(branch))
818 branchname = urlreq.quote(encoding.fromlocal(branch))
819 branchnodes = encodelist(nodes)
819 branchnodes = encodelist(nodes)
820 heads.append('%s %s' % (branchname, branchnodes))
820 heads.append('%s %s' % (branchname, branchnodes))
821
821
822 return wireprototypes.bytesresponse('\n'.join(heads))
822 return wireprototypes.bytesresponse('\n'.join(heads))
823
823
824 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
824 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY,
825 permission='pull')
825 permission='pull')
826 def branches(repo, proto, nodes):
826 def branches(repo, proto, nodes):
827 nodes = decodelist(nodes)
827 nodes = decodelist(nodes)
828 r = []
828 r = []
829 for b in repo.branches(nodes):
829 for b in repo.branches(nodes):
830 r.append(encodelist(b) + "\n")
830 r.append(encodelist(b) + "\n")
831
831
832 return wireprototypes.bytesresponse(''.join(r))
832 return wireprototypes.bytesresponse(''.join(r))
833
833
834 @wireprotocommand('clonebundles', '', permission='pull')
834 @wireprotocommand('clonebundles', '', permission='pull')
835 def clonebundles(repo, proto):
835 def clonebundles(repo, proto):
836 """Server command for returning info for available bundles to seed clones.
836 """Server command for returning info for available bundles to seed clones.
837
837
838 Clients will parse this response and determine what bundle to fetch.
838 Clients will parse this response and determine what bundle to fetch.
839
839
840 Extensions may wrap this command to filter or dynamically emit data
840 Extensions may wrap this command to filter or dynamically emit data
841 depending on the request. e.g. you could advertise URLs for the closest
841 depending on the request. e.g. you could advertise URLs for the closest
842 data center given the client's IP address.
842 data center given the client's IP address.
843 """
843 """
844 return wireprototypes.bytesresponse(
844 return wireprototypes.bytesresponse(
845 repo.vfs.tryread('clonebundles.manifest'))
845 repo.vfs.tryread('clonebundles.manifest'))
846
846
847 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
847 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
848 'known', 'getbundle', 'unbundlehash']
848 'known', 'getbundle', 'unbundlehash']
849
849
850 def _capabilities(repo, proto):
850 def _capabilities(repo, proto):
851 """return a list of capabilities for a repo
851 """return a list of capabilities for a repo
852
852
853 This function exists to allow extensions to easily wrap capabilities
853 This function exists to allow extensions to easily wrap capabilities
854 computation
854 computation
855
855
856 - returns a lists: easy to alter
856 - returns a lists: easy to alter
857 - change done here will be propagated to both `capabilities` and `hello`
857 - change done here will be propagated to both `capabilities` and `hello`
858 command without any other action needed.
858 command without any other action needed.
859 """
859 """
860 # copy to prevent modification of the global list
860 # copy to prevent modification of the global list
861 caps = list(wireprotocaps)
861 caps = list(wireprotocaps)
862
862
863 # Command of same name as capability isn't exposed to version 1 of
863 # Command of same name as capability isn't exposed to version 1 of
864 # transports. So conditionally add it.
864 # transports. So conditionally add it.
865 if commands.commandavailable('changegroupsubset', proto):
865 if commands.commandavailable('changegroupsubset', proto):
866 caps.append('changegroupsubset')
866 caps.append('changegroupsubset')
867
867
868 if streamclone.allowservergeneration(repo):
868 if streamclone.allowservergeneration(repo):
869 if repo.ui.configbool('server', 'preferuncompressed'):
869 if repo.ui.configbool('server', 'preferuncompressed'):
870 caps.append('stream-preferred')
870 caps.append('stream-preferred')
871 requiredformats = repo.requirements & repo.supportedformats
871 requiredformats = repo.requirements & repo.supportedformats
872 # if our local revlogs are just revlogv1, add 'stream' cap
872 # if our local revlogs are just revlogv1, add 'stream' cap
873 if not requiredformats - {'revlogv1'}:
873 if not requiredformats - {'revlogv1'}:
874 caps.append('stream')
874 caps.append('stream')
875 # otherwise, add 'streamreqs' detailing our local revlog format
875 # otherwise, add 'streamreqs' detailing our local revlog format
876 else:
876 else:
877 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
877 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
878 if repo.ui.configbool('experimental', 'bundle2-advertise'):
878 if repo.ui.configbool('experimental', 'bundle2-advertise'):
879 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
879 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
880 caps.append('bundle2=' + urlreq.quote(capsblob))
880 caps.append('bundle2=' + urlreq.quote(capsblob))
881 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
881 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
882
882
883 return proto.addcapabilities(repo, caps)
883 return proto.addcapabilities(repo, caps)
884
884
885 # If you are writing an extension and consider wrapping this function. Wrap
885 # If you are writing an extension and consider wrapping this function. Wrap
886 # `_capabilities` instead.
886 # `_capabilities` instead.
887 @wireprotocommand('capabilities', permission='pull')
887 @wireprotocommand('capabilities', permission='pull')
888 def capabilities(repo, proto):
888 def capabilities(repo, proto):
889 return wireprototypes.bytesresponse(' '.join(_capabilities(repo, proto)))
889 return wireprototypes.bytesresponse(' '.join(_capabilities(repo, proto)))
890
890
891 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
891 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY,
892 permission='pull')
892 permission='pull')
893 def changegroup(repo, proto, roots):
893 def changegroup(repo, proto, roots):
894 nodes = decodelist(roots)
894 nodes = decodelist(roots)
895 outgoing = discovery.outgoing(repo, missingroots=nodes,
895 outgoing = discovery.outgoing(repo, missingroots=nodes,
896 missingheads=repo.heads())
896 missingheads=repo.heads())
897 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
897 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
898 gen = iter(lambda: cg.read(32768), '')
898 gen = iter(lambda: cg.read(32768), '')
899 return wireprototypes.streamres(gen=gen)
899 return wireprototypes.streamres(gen=gen)
900
900
901 @wireprotocommand('changegroupsubset', 'bases heads',
901 @wireprotocommand('changegroupsubset', 'bases heads',
902 transportpolicy=POLICY_V1_ONLY,
902 transportpolicy=POLICY_V1_ONLY,
903 permission='pull')
903 permission='pull')
904 def changegroupsubset(repo, proto, bases, heads):
904 def changegroupsubset(repo, proto, bases, heads):
905 bases = decodelist(bases)
905 bases = decodelist(bases)
906 heads = decodelist(heads)
906 heads = decodelist(heads)
907 outgoing = discovery.outgoing(repo, missingroots=bases,
907 outgoing = discovery.outgoing(repo, missingroots=bases,
908 missingheads=heads)
908 missingheads=heads)
909 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
909 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
910 gen = iter(lambda: cg.read(32768), '')
910 gen = iter(lambda: cg.read(32768), '')
911 return wireprototypes.streamres(gen=gen)
911 return wireprototypes.streamres(gen=gen)
912
912
913 @wireprotocommand('debugwireargs', 'one two *',
913 @wireprotocommand('debugwireargs', 'one two *',
914 permission='pull')
914 permission='pull')
915 def debugwireargs(repo, proto, one, two, others):
915 def debugwireargs(repo, proto, one, two, others):
916 # only accept optional args from the known set
916 # only accept optional args from the known set
917 opts = options('debugwireargs', ['three', 'four'], others)
917 opts = options('debugwireargs', ['three', 'four'], others)
918 return wireprototypes.bytesresponse(repo.debugwireargs(
918 return wireprototypes.bytesresponse(repo.debugwireargs(
919 one, two, **pycompat.strkwargs(opts)))
919 one, two, **pycompat.strkwargs(opts)))
920
920
921 @wireprotocommand('getbundle', '*', permission='pull')
921 @wireprotocommand('getbundle', '*', permission='pull')
922 def getbundle(repo, proto, others):
922 def getbundle(repo, proto, others):
923 opts = options('getbundle', gboptsmap.keys(), others)
923 opts = options('getbundle', gboptsmap.keys(), others)
924 for k, v in opts.iteritems():
924 for k, v in opts.iteritems():
925 keytype = gboptsmap[k]
925 keytype = gboptsmap[k]
926 if keytype == 'nodes':
926 if keytype == 'nodes':
927 opts[k] = decodelist(v)
927 opts[k] = decodelist(v)
928 elif keytype == 'csv':
928 elif keytype == 'csv':
929 opts[k] = list(v.split(','))
929 opts[k] = list(v.split(','))
930 elif keytype == 'scsv':
930 elif keytype == 'scsv':
931 opts[k] = set(v.split(','))
931 opts[k] = set(v.split(','))
932 elif keytype == 'boolean':
932 elif keytype == 'boolean':
933 # Client should serialize False as '0', which is a non-empty string
933 # Client should serialize False as '0', which is a non-empty string
934 # so it evaluates as a True bool.
934 # so it evaluates as a True bool.
935 if v == '0':
935 if v == '0':
936 opts[k] = False
936 opts[k] = False
937 else:
937 else:
938 opts[k] = bool(v)
938 opts[k] = bool(v)
939 elif keytype != 'plain':
939 elif keytype != 'plain':
940 raise KeyError('unknown getbundle option type %s'
940 raise KeyError('unknown getbundle option type %s'
941 % keytype)
941 % keytype)
942
942
943 if not bundle1allowed(repo, 'pull'):
943 if not bundle1allowed(repo, 'pull'):
944 if not exchange.bundle2requested(opts.get('bundlecaps')):
944 if not exchange.bundle2requested(opts.get('bundlecaps')):
945 if proto.name == 'http-v1':
945 if proto.name == 'http-v1':
946 return wireprototypes.ooberror(bundle2required)
946 return wireprototypes.ooberror(bundle2required)
947 raise error.Abort(bundle2requiredmain,
947 raise error.Abort(bundle2requiredmain,
948 hint=bundle2requiredhint)
948 hint=bundle2requiredhint)
949
949
950 prefercompressed = True
950 prefercompressed = True
951
951
952 try:
952 try:
953 if repo.ui.configbool('server', 'disablefullbundle'):
953 if repo.ui.configbool('server', 'disablefullbundle'):
954 # Check to see if this is a full clone.
954 # Check to see if this is a full clone.
955 clheads = set(repo.changelog.heads())
955 clheads = set(repo.changelog.heads())
956 changegroup = opts.get('cg', True)
956 changegroup = opts.get('cg', True)
957 heads = set(opts.get('heads', set()))
957 heads = set(opts.get('heads', set()))
958 common = set(opts.get('common', set()))
958 common = set(opts.get('common', set()))
959 common.discard(nullid)
959 common.discard(nullid)
960 if changegroup and not common and clheads == heads:
960 if changegroup and not common and clheads == heads:
961 raise error.Abort(
961 raise error.Abort(
962 _('server has pull-based clones disabled'),
962 _('server has pull-based clones disabled'),
963 hint=_('remove --pull if specified or upgrade Mercurial'))
963 hint=_('remove --pull if specified or upgrade Mercurial'))
964
964
965 info, chunks = exchange.getbundlechunks(repo, 'serve',
965 info, chunks = exchange.getbundlechunks(repo, 'serve',
966 **pycompat.strkwargs(opts))
966 **pycompat.strkwargs(opts))
967 prefercompressed = info.get('prefercompressed', True)
967 prefercompressed = info.get('prefercompressed', True)
968 except error.Abort as exc:
968 except error.Abort as exc:
969 # cleanly forward Abort error to the client
969 # cleanly forward Abort error to the client
970 if not exchange.bundle2requested(opts.get('bundlecaps')):
970 if not exchange.bundle2requested(opts.get('bundlecaps')):
971 if proto.name == 'http-v1':
971 if proto.name == 'http-v1':
972 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
972 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
973 raise # cannot do better for bundle1 + ssh
973 raise # cannot do better for bundle1 + ssh
974 # bundle2 request expect a bundle2 reply
974 # bundle2 request expect a bundle2 reply
975 bundler = bundle2.bundle20(repo.ui)
975 bundler = bundle2.bundle20(repo.ui)
976 manargs = [('message', pycompat.bytestr(exc))]
976 manargs = [('message', pycompat.bytestr(exc))]
977 advargs = []
977 advargs = []
978 if exc.hint is not None:
978 if exc.hint is not None:
979 advargs.append(('hint', exc.hint))
979 advargs.append(('hint', exc.hint))
980 bundler.addpart(bundle2.bundlepart('error:abort',
980 bundler.addpart(bundle2.bundlepart('error:abort',
981 manargs, advargs))
981 manargs, advargs))
982 chunks = bundler.getchunks()
982 chunks = bundler.getchunks()
983 prefercompressed = False
983 prefercompressed = False
984
984
985 return wireprototypes.streamres(
985 return wireprototypes.streamres(
986 gen=chunks, prefer_uncompressed=not prefercompressed)
986 gen=chunks, prefer_uncompressed=not prefercompressed)
987
987
988 @wireprotocommand('heads', permission='pull')
988 @wireprotocommand('heads', permission='pull')
989 def heads(repo, proto):
989 def heads(repo, proto):
990 h = repo.heads()
990 h = repo.heads()
991 return wireprototypes.bytesresponse(encodelist(h) + '\n')
991 return wireprototypes.bytesresponse(encodelist(h) + '\n')
992
992
993 @wireprotocommand('hello', permission='pull')
993 @wireprotocommand('hello', permission='pull')
994 def hello(repo, proto):
994 def hello(repo, proto):
995 """Called as part of SSH handshake to obtain server info.
995 """Called as part of SSH handshake to obtain server info.
996
996
997 Returns a list of lines describing interesting things about the
997 Returns a list of lines describing interesting things about the
998 server, in an RFC822-like format.
998 server, in an RFC822-like format.
999
999
1000 Currently, the only one defined is ``capabilities``, which consists of a
1000 Currently, the only one defined is ``capabilities``, which consists of a
1001 line of space separated tokens describing server abilities:
1001 line of space separated tokens describing server abilities:
1002
1002
1003 capabilities: <token0> <token1> <token2>
1003 capabilities: <token0> <token1> <token2>
1004 """
1004 """
1005 caps = capabilities(repo, proto).data
1005 caps = capabilities(repo, proto).data
1006 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
1006 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
1007
1007
1008 @wireprotocommand('listkeys', 'namespace', permission='pull')
1008 @wireprotocommand('listkeys', 'namespace', permission='pull')
1009 def listkeys(repo, proto, namespace):
1009 def listkeys(repo, proto, namespace):
1010 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
1010 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
1011 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
1011 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
1012
1012
1013 @wireprotocommand('lookup', 'key', permission='pull')
1013 @wireprotocommand('lookup', 'key', permission='pull')
1014 def lookup(repo, proto, key):
1014 def lookup(repo, proto, key):
1015 try:
1015 try:
1016 k = encoding.tolocal(key)
1016 k = encoding.tolocal(key)
1017 c = repo[k]
1017 c = repo[k]
1018 r = c.hex()
1018 r = c.hex()
1019 success = 1
1019 success = 1
1020 except Exception as inst:
1020 except Exception as inst:
1021 r = stringutil.forcebytestr(inst)
1021 r = stringutil.forcebytestr(inst)
1022 success = 0
1022 success = 0
1023 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
1023 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
1024
1024
1025 @wireprotocommand('known', 'nodes *', permission='pull')
1025 @wireprotocommand('known', 'nodes *', permission='pull')
1026 def known(repo, proto, nodes, others):
1026 def known(repo, proto, nodes, others):
1027 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
1027 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
1028 return wireprototypes.bytesresponse(v)
1028 return wireprototypes.bytesresponse(v)
1029
1029
1030 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
1030 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
1031 def pushkey(repo, proto, namespace, key, old, new):
1031 def pushkey(repo, proto, namespace, key, old, new):
1032 # compatibility with pre-1.8 clients which were accidentally
1032 # compatibility with pre-1.8 clients which were accidentally
1033 # sending raw binary nodes rather than utf-8-encoded hex
1033 # sending raw binary nodes rather than utf-8-encoded hex
1034 if len(new) == 20 and stringutil.escapestr(new) != new:
1034 if len(new) == 20 and stringutil.escapestr(new) != new:
1035 # looks like it could be a binary node
1035 # looks like it could be a binary node
1036 try:
1036 try:
1037 new.decode('utf-8')
1037 new.decode('utf-8')
1038 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
1038 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
1039 except UnicodeDecodeError:
1039 except UnicodeDecodeError:
1040 pass # binary, leave unmodified
1040 pass # binary, leave unmodified
1041 else:
1041 else:
1042 new = encoding.tolocal(new) # normal path
1042 new = encoding.tolocal(new) # normal path
1043
1043
1044 with proto.mayberedirectstdio() as output:
1044 with proto.mayberedirectstdio() as output:
1045 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
1045 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
1046 encoding.tolocal(old), new) or False
1046 encoding.tolocal(old), new) or False
1047
1047
1048 output = output.getvalue() if output else ''
1048 output = output.getvalue() if output else ''
1049 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
1049 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
1050
1050
1051 @wireprotocommand('stream_out', permission='pull')
1051 @wireprotocommand('stream_out', permission='pull')
1052 def stream(repo, proto):
1052 def stream(repo, proto):
1053 '''If the server supports streaming clone, it advertises the "stream"
1053 '''If the server supports streaming clone, it advertises the "stream"
1054 capability with a value representing the version and flags of the repo
1054 capability with a value representing the version and flags of the repo
1055 it is serving. Client checks to see if it understands the format.
1055 it is serving. Client checks to see if it understands the format.
1056 '''
1056 '''
1057 return wireprototypes.streamreslegacy(
1057 return wireprototypes.streamreslegacy(
1058 streamclone.generatev1wireproto(repo))
1058 streamclone.generatev1wireproto(repo))
1059
1059
1060 @wireprotocommand('unbundle', 'heads', permission='push')
1060 @wireprotocommand('unbundle', 'heads', permission='push')
1061 def unbundle(repo, proto, heads):
1061 def unbundle(repo, proto, heads):
1062 their_heads = decodelist(heads)
1062 their_heads = decodelist(heads)
1063
1063
1064 with proto.mayberedirectstdio() as output:
1064 with proto.mayberedirectstdio() as output:
1065 try:
1065 try:
1066 exchange.check_heads(repo, their_heads, 'preparing changes')
1066 exchange.check_heads(repo, their_heads, 'preparing changes')
1067
1067
1068 # write bundle data to temporary file because it can be big
1068 # write bundle data to temporary file because it can be big
1069 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1069 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1070 fp = os.fdopen(fd, r'wb+')
1070 fp = os.fdopen(fd, r'wb+')
1071 r = 0
1071 r = 0
1072 try:
1072 try:
1073 proto.forwardpayload(fp)
1073 proto.forwardpayload(fp)
1074 fp.seek(0)
1074 fp.seek(0)
1075 gen = exchange.readbundle(repo.ui, fp, None)
1075 gen = exchange.readbundle(repo.ui, fp, None)
1076 if (isinstance(gen, changegroupmod.cg1unpacker)
1076 if (isinstance(gen, changegroupmod.cg1unpacker)
1077 and not bundle1allowed(repo, 'push')):
1077 and not bundle1allowed(repo, 'push')):
1078 if proto.name == 'http-v1':
1078 if proto.name == 'http-v1':
1079 # need to special case http because stderr do not get to
1079 # need to special case http because stderr do not get to
1080 # the http client on failed push so we need to abuse
1080 # the http client on failed push so we need to abuse
1081 # some other error type to make sure the message get to
1081 # some other error type to make sure the message get to
1082 # the user.
1082 # the user.
1083 return wireprototypes.ooberror(bundle2required)
1083 return wireprototypes.ooberror(bundle2required)
1084 raise error.Abort(bundle2requiredmain,
1084 raise error.Abort(bundle2requiredmain,
1085 hint=bundle2requiredhint)
1085 hint=bundle2requiredhint)
1086
1086
1087 r = exchange.unbundle(repo, gen, their_heads, 'serve',
1087 r = exchange.unbundle(repo, gen, their_heads, 'serve',
1088 proto.client())
1088 proto.client())
1089 if util.safehasattr(r, 'addpart'):
1089 if util.safehasattr(r, 'addpart'):
1090 # The return looks streamable, we are in the bundle2 case
1090 # The return looks streamable, we are in the bundle2 case
1091 # and should return a stream.
1091 # and should return a stream.
1092 return wireprototypes.streamreslegacy(gen=r.getchunks())
1092 return wireprototypes.streamreslegacy(gen=r.getchunks())
1093 return wireprototypes.pushres(
1093 return wireprototypes.pushres(
1094 r, output.getvalue() if output else '')
1094 r, output.getvalue() if output else '')
1095
1095
1096 finally:
1096 finally:
1097 fp.close()
1097 fp.close()
1098 os.unlink(tempname)
1098 os.unlink(tempname)
1099
1099
1100 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1100 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1101 # handle non-bundle2 case first
1101 # handle non-bundle2 case first
1102 if not getattr(exc, 'duringunbundle2', False):
1102 if not getattr(exc, 'duringunbundle2', False):
1103 try:
1103 try:
1104 raise
1104 raise
1105 except error.Abort:
1105 except error.Abort:
1106 # The old code we moved used procutil.stderr directly.
1106 # The old code we moved used procutil.stderr directly.
1107 # We did not change it to minimise code change.
1107 # We did not change it to minimise code change.
1108 # This need to be moved to something proper.
1108 # This need to be moved to something proper.
1109 # Feel free to do it.
1109 # Feel free to do it.
1110 procutil.stderr.write("abort: %s\n" % exc)
1110 procutil.stderr.write("abort: %s\n" % exc)
1111 if exc.hint is not None:
1111 if exc.hint is not None:
1112 procutil.stderr.write("(%s)\n" % exc.hint)
1112 procutil.stderr.write("(%s)\n" % exc.hint)
1113 procutil.stderr.flush()
1113 procutil.stderr.flush()
1114 return wireprototypes.pushres(
1114 return wireprototypes.pushres(
1115 0, output.getvalue() if output else '')
1115 0, output.getvalue() if output else '')
1116 except error.PushRaced:
1116 except error.PushRaced:
1117 return wireprototypes.pusherr(
1117 return wireprototypes.pusherr(
1118 pycompat.bytestr(exc),
1118 pycompat.bytestr(exc),
1119 output.getvalue() if output else '')
1119 output.getvalue() if output else '')
1120
1120
1121 bundler = bundle2.bundle20(repo.ui)
1121 bundler = bundle2.bundle20(repo.ui)
1122 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1122 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1123 bundler.addpart(out)
1123 bundler.addpart(out)
1124 try:
1124 try:
1125 try:
1125 try:
1126 raise
1126 raise
1127 except error.PushkeyFailed as exc:
1127 except error.PushkeyFailed as exc:
1128 # check client caps
1128 # check client caps
1129 remotecaps = getattr(exc, '_replycaps', None)
1129 remotecaps = getattr(exc, '_replycaps', None)
1130 if (remotecaps is not None
1130 if (remotecaps is not None
1131 and 'pushkey' not in remotecaps.get('error', ())):
1131 and 'pushkey' not in remotecaps.get('error', ())):
1132 # no support remote side, fallback to Abort handler.
1132 # no support remote side, fallback to Abort handler.
1133 raise
1133 raise
1134 part = bundler.newpart('error:pushkey')
1134 part = bundler.newpart('error:pushkey')
1135 part.addparam('in-reply-to', exc.partid)
1135 part.addparam('in-reply-to', exc.partid)
1136 if exc.namespace is not None:
1136 if exc.namespace is not None:
1137 part.addparam('namespace', exc.namespace,
1137 part.addparam('namespace', exc.namespace,
1138 mandatory=False)
1138 mandatory=False)
1139 if exc.key is not None:
1139 if exc.key is not None:
1140 part.addparam('key', exc.key, mandatory=False)
1140 part.addparam('key', exc.key, mandatory=False)
1141 if exc.new is not None:
1141 if exc.new is not None:
1142 part.addparam('new', exc.new, mandatory=False)
1142 part.addparam('new', exc.new, mandatory=False)
1143 if exc.old is not None:
1143 if exc.old is not None:
1144 part.addparam('old', exc.old, mandatory=False)
1144 part.addparam('old', exc.old, mandatory=False)
1145 if exc.ret is not None:
1145 if exc.ret is not None:
1146 part.addparam('ret', exc.ret, mandatory=False)
1146 part.addparam('ret', exc.ret, mandatory=False)
1147 except error.BundleValueError as exc:
1147 except error.BundleValueError as exc:
1148 errpart = bundler.newpart('error:unsupportedcontent')
1148 errpart = bundler.newpart('error:unsupportedcontent')
1149 if exc.parttype is not None:
1149 if exc.parttype is not None:
1150 errpart.addparam('parttype', exc.parttype)
1150 errpart.addparam('parttype', exc.parttype)
1151 if exc.params:
1151 if exc.params:
1152 errpart.addparam('params', '\0'.join(exc.params))
1152 errpart.addparam('params', '\0'.join(exc.params))
1153 except error.Abort as exc:
1153 except error.Abort as exc:
1154 manargs = [('message', stringutil.forcebytestr(exc))]
1154 manargs = [('message', stringutil.forcebytestr(exc))]
1155 advargs = []
1155 advargs = []
1156 if exc.hint is not None:
1156 if exc.hint is not None:
1157 advargs.append(('hint', exc.hint))
1157 advargs.append(('hint', exc.hint))
1158 bundler.addpart(bundle2.bundlepart('error:abort',
1158 bundler.addpart(bundle2.bundlepart('error:abort',
1159 manargs, advargs))
1159 manargs, advargs))
1160 except error.PushRaced as exc:
1160 except error.PushRaced as exc:
1161 bundler.newpart('error:pushraced',
1161 bundler.newpart('error:pushraced',
1162 [('message', stringutil.forcebytestr(exc))])
1162 [('message', stringutil.forcebytestr(exc))])
1163 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
1163 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
@@ -1,146 +1,135 b''
1 # Test that certain objects conform to well-defined interfaces.
1 # Test that certain objects conform to well-defined interfaces.
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import os
5 import os
6
6
7 from mercurial.thirdparty.zope import (
7 from mercurial.thirdparty.zope import (
8 interface as zi,
8 interface as zi,
9 )
9 )
10 from mercurial.thirdparty.zope.interface import (
10 from mercurial.thirdparty.zope.interface import (
11 verify as ziverify,
11 verify as ziverify,
12 )
12 )
13 from mercurial import (
13 from mercurial import (
14 bundlerepo,
14 bundlerepo,
15 httppeer,
15 httppeer,
16 localrepo,
16 localrepo,
17 repository,
17 repository,
18 sshpeer,
18 sshpeer,
19 statichttprepo,
19 statichttprepo,
20 ui as uimod,
20 ui as uimod,
21 unionrepo,
21 unionrepo,
22 wireprotoserver,
22 wireprotoserver,
23 wireprototypes,
23 wireprototypes,
24 )
24 )
25
25
26 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
26 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
27
27
28 def checkobject(o):
29 """Verify a constructed object conforms to interface rules.
30
31 An object must have __abstractmethods__ defined.
32
33 All "public" attributes of the object (attributes not prefixed with
34 an underscore) must be in __abstractmethods__ or appear on a base class
35 with __abstractmethods__.
36 """
37 name = o.__class__.__name__
38
39 allowed = set()
40 for cls in o.__class__.__mro__:
41 if not getattr(cls, '__abstractmethods__', set()):
42 continue
43
44 allowed |= cls.__abstractmethods__
45 allowed |= {a for a in dir(cls) if not a.startswith('_')}
46
47 if not allowed:
48 print('%s does not have abstract methods' % name)
49 return
50
51 public = {a for a in dir(o) if not a.startswith('_')}
52
53 for attr in sorted(public - allowed):
54 print('public attributes not in abstract interface: %s.%s' % (
55 name, attr))
56
57 def checkzobject(o):
28 def checkzobject(o):
58 """Verify an object with a zope interface."""
29 """Verify an object with a zope interface."""
59 ifaces = zi.providedBy(o)
30 ifaces = zi.providedBy(o)
60 if not ifaces:
31 if not ifaces:
61 print('%r does not provide any zope interfaces' % o)
32 print('%r does not provide any zope interfaces' % o)
62 return
33 return
63
34
64 # Run zope.interface's built-in verification routine. This verifies that
35 # Run zope.interface's built-in verification routine. This verifies that
65 # everything that is supposed to be present is present.
36 # everything that is supposed to be present is present.
66 for iface in ifaces:
37 for iface in ifaces:
67 ziverify.verifyObject(iface, o)
38 ziverify.verifyObject(iface, o)
68
39
69 # Now verify that the object provides no extra public attributes that
40 # Now verify that the object provides no extra public attributes that
70 # aren't declared as part of interfaces.
41 # aren't declared as part of interfaces.
71 allowed = set()
42 allowed = set()
72 for iface in ifaces:
43 for iface in ifaces:
73 allowed |= set(iface.names(all=True))
44 allowed |= set(iface.names(all=True))
74
45
75 public = {a for a in dir(o) if not a.startswith('_')}
46 public = {a for a in dir(o) if not a.startswith('_')}
76
47
77 for attr in sorted(public - allowed):
48 for attr in sorted(public - allowed):
78 print('public attribute not declared in interfaces: %s.%s' % (
49 print('public attribute not declared in interfaces: %s.%s' % (
79 o.__class__.__name__, attr))
50 o.__class__.__name__, attr))
80
51
81 # Facilitates testing localpeer.
52 # Facilitates testing localpeer.
82 class dummyrepo(object):
53 class dummyrepo(object):
83 def __init__(self):
54 def __init__(self):
84 self.ui = uimod.ui()
55 self.ui = uimod.ui()
85 def filtered(self, name):
56 def filtered(self, name):
86 pass
57 pass
87 def _restrictcapabilities(self, caps):
58 def _restrictcapabilities(self, caps):
88 pass
59 pass
89
60
90 class dummyopener(object):
61 class dummyopener(object):
91 handlers = []
62 handlers = []
92
63
93 # Facilitates testing sshpeer without requiring an SSH server.
64 # Facilitates testing sshpeer without requiring an SSH server.
94 class badpeer(httppeer.httppeer):
65 class badpeer(httppeer.httppeer):
95 def __init__(self):
66 def __init__(self):
96 super(badpeer, self).__init__(None, None, None, dummyopener())
67 super(badpeer, self).__init__(None, None, None, dummyopener())
97 self.badattribute = True
68 self.badattribute = True
98
69
99 def badmethod(self):
70 def badmethod(self):
100 pass
71 pass
101
72
102 class dummypipe(object):
73 class dummypipe(object):
103 def close(self):
74 def close(self):
104 pass
75 pass
105
76
106 def main():
77 def main():
107 ui = uimod.ui()
78 ui = uimod.ui()
108 # Needed so we can open a local repo with obsstore without a warning.
79 # Needed so we can open a local repo with obsstore without a warning.
109 ui.setconfig('experimental', 'evolution.createmarkers', True)
80 ui.setconfig('experimental', 'evolution.createmarkers', True)
110
81
111 checkobject(badpeer())
82 checkzobject(badpeer())
112 checkobject(httppeer.httppeer(None, None, None, dummyopener()))
83
113 checkobject(localrepo.localpeer(dummyrepo()))
84 ziverify.verifyClass(repository.ipeerbaselegacycommands,
114 checkobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
85 httppeer.httppeer)
115 dummypipe(), None, None))
86 checkzobject(httppeer.httppeer(None, None, None, dummyopener()))
116 checkobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
87
117 dummypipe(), None, None))
88 ziverify.verifyClass(repository.ipeerbase,
118 checkobject(bundlerepo.bundlepeer(dummyrepo()))
89 localrepo.localpeer)
119 checkobject(statichttprepo.statichttppeer(dummyrepo()))
90 checkzobject(localrepo.localpeer(dummyrepo()))
120 checkobject(unionrepo.unionpeer(dummyrepo()))
91
92 ziverify.verifyClass(repository.ipeerbaselegacycommands,
93 sshpeer.sshv1peer)
94 checkzobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
95 dummypipe(), None, None))
96
97 ziverify.verifyClass(repository.ipeerbaselegacycommands,
98 sshpeer.sshv2peer)
99 checkzobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
100 dummypipe(), None, None))
101
102 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
103 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
104
105 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
106 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
107
108 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
109 checkzobject(unionrepo.unionpeer(dummyrepo()))
121
110
122 ziverify.verifyClass(repository.completelocalrepository,
111 ziverify.verifyClass(repository.completelocalrepository,
123 localrepo.localrepository)
112 localrepo.localrepository)
124 repo = localrepo.localrepository(ui, rootdir)
113 repo = localrepo.localrepository(ui, rootdir)
125 checkzobject(repo)
114 checkzobject(repo)
126
115
127 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
116 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
128 wireprotoserver.sshv1protocolhandler)
117 wireprotoserver.sshv1protocolhandler)
129 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
118 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
130 wireprotoserver.sshv2protocolhandler)
119 wireprotoserver.sshv2protocolhandler)
131 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
120 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
132 wireprotoserver.httpv1protocolhandler)
121 wireprotoserver.httpv1protocolhandler)
133 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
122 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
134 wireprotoserver.httpv2protocolhandler)
123 wireprotoserver.httpv2protocolhandler)
135
124
136 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
125 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
137 checkzobject(sshv1)
126 checkzobject(sshv1)
138 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
127 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
139 checkzobject(sshv2)
128 checkzobject(sshv2)
140
129
141 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
130 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
142 checkzobject(httpv1)
131 checkzobject(httpv1)
143 httpv2 = wireprotoserver.httpv2protocolhandler(None, None)
132 httpv2 = wireprotoserver.httpv2protocolhandler(None, None)
144 checkzobject(httpv2)
133 checkzobject(httpv2)
145
134
146 main()
135 main()
@@ -1,2 +1,2 b''
1 public attributes not in abstract interface: badpeer.badattribute
1 public attribute not declared in interfaces: badpeer.badattribute
2 public attributes not in abstract interface: badpeer.badmethod
2 public attribute not declared in interfaces: badpeer.badmethod
General Comments 0
You need to be logged in to leave comments. Login now