##// END OF EJS Templates
httppeer: implement ipeerconnection...
Gregory Szorc -
r37627:01bfe5ad default
parent child Browse files
Show More
@@ -1,767 +1,789 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 .thirdparty import (
19 from .thirdparty import (
20 cbor,
20 cbor,
21 )
21 )
22 from .thirdparty.zope import (
23 interface as zi,
24 )
22 from . import (
25 from . import (
23 bundle2,
26 bundle2,
24 error,
27 error,
25 httpconnection,
28 httpconnection,
26 pycompat,
29 pycompat,
30 repository,
27 statichttprepo,
31 statichttprepo,
28 url as urlmod,
32 url as urlmod,
29 util,
33 util,
30 wireproto,
34 wireproto,
31 wireprotoframing,
35 wireprotoframing,
32 wireprototypes,
36 wireprototypes,
33 wireprotov2server,
37 wireprotov2server,
34 )
38 )
35
39
36 httplib = util.httplib
40 httplib = util.httplib
37 urlerr = util.urlerr
41 urlerr = util.urlerr
38 urlreq = util.urlreq
42 urlreq = util.urlreq
39
43
40 def encodevalueinheaders(value, header, limit):
44 def encodevalueinheaders(value, header, limit):
41 """Encode a string value into multiple HTTP headers.
45 """Encode a string value into multiple HTTP headers.
42
46
43 ``value`` will be encoded into 1 or more HTTP headers with the names
47 ``value`` will be encoded into 1 or more HTTP headers with the names
44 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
45 name + value will be at most ``limit`` bytes long.
49 name + value will be at most ``limit`` bytes long.
46
50
47 Returns an iterable of 2-tuples consisting of header names and
51 Returns an iterable of 2-tuples consisting of header names and
48 values as native strings.
52 values as native strings.
49 """
53 """
50 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
51 # not bytes. This function always takes bytes in as arguments.
55 # not bytes. This function always takes bytes in as arguments.
52 fmt = pycompat.strurl(header) + r'-%s'
56 fmt = pycompat.strurl(header) + r'-%s'
53 # Note: it is *NOT* a bug that the last bit here is a bytestring
57 # Note: it is *NOT* a bug that the last bit here is a bytestring
54 # and not a unicode: we're just getting the encoded length anyway,
58 # and not a unicode: we're just getting the encoded length anyway,
55 # and using an r-string to make it portable between Python 2 and 3
59 # and using an r-string to make it portable between Python 2 and 3
56 # doesn't work because then the \r is a literal backslash-r
60 # doesn't work because then the \r is a literal backslash-r
57 # instead of a carriage return.
61 # instead of a carriage return.
58 valuelen = limit - len(fmt % r'000') - len(': \r\n')
62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
59 result = []
63 result = []
60
64
61 n = 0
65 n = 0
62 for i in xrange(0, len(value), valuelen):
66 for i in xrange(0, len(value), valuelen):
63 n += 1
67 n += 1
64 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
65
69
66 return result
70 return result
67
71
68 def _wraphttpresponse(resp):
72 def _wraphttpresponse(resp):
69 """Wrap an HTTPResponse with common error handlers.
73 """Wrap an HTTPResponse with common error handlers.
70
74
71 This ensures that any I/O from any consumer raises the appropriate
75 This ensures that any I/O from any consumer raises the appropriate
72 error and messaging.
76 error and messaging.
73 """
77 """
74 origread = resp.read
78 origread = resp.read
75
79
76 class readerproxy(resp.__class__):
80 class readerproxy(resp.__class__):
77 def read(self, size=None):
81 def read(self, size=None):
78 try:
82 try:
79 return origread(size)
83 return origread(size)
80 except httplib.IncompleteRead as e:
84 except httplib.IncompleteRead as e:
81 # e.expected is an integer if length known or None otherwise.
85 # e.expected is an integer if length known or None otherwise.
82 if e.expected:
86 if e.expected:
83 msg = _('HTTP request error (incomplete response; '
87 msg = _('HTTP request error (incomplete response; '
84 'expected %d bytes got %d)') % (e.expected,
88 'expected %d bytes got %d)') % (e.expected,
85 len(e.partial))
89 len(e.partial))
86 else:
90 else:
87 msg = _('HTTP request error (incomplete response)')
91 msg = _('HTTP request error (incomplete response)')
88
92
89 raise error.PeerTransportError(
93 raise error.PeerTransportError(
90 msg,
94 msg,
91 hint=_('this may be an intermittent network failure; '
95 hint=_('this may be an intermittent network failure; '
92 'if the error persists, consider contacting the '
96 'if the error persists, consider contacting the '
93 'network or server operator'))
97 'network or server operator'))
94 except httplib.HTTPException as e:
98 except httplib.HTTPException as e:
95 raise error.PeerTransportError(
99 raise error.PeerTransportError(
96 _('HTTP request error (%s)') % e,
100 _('HTTP request error (%s)') % e,
97 hint=_('this may be an intermittent network failure; '
101 hint=_('this may be an intermittent network failure; '
98 'if the error persists, consider contacting the '
102 'if the error persists, consider contacting the '
99 'network or server operator'))
103 'network or server operator'))
100
104
101 resp.__class__ = readerproxy
105 resp.__class__ = readerproxy
102
106
103 class _multifile(object):
107 class _multifile(object):
104 def __init__(self, *fileobjs):
108 def __init__(self, *fileobjs):
105 for f in fileobjs:
109 for f in fileobjs:
106 if not util.safehasattr(f, 'length'):
110 if not util.safehasattr(f, 'length'):
107 raise ValueError(
111 raise ValueError(
108 '_multifile only supports file objects that '
112 '_multifile only supports file objects that '
109 'have a length but this one does not:', type(f), f)
113 'have a length but this one does not:', type(f), f)
110 self._fileobjs = fileobjs
114 self._fileobjs = fileobjs
111 self._index = 0
115 self._index = 0
112
116
113 @property
117 @property
114 def length(self):
118 def length(self):
115 return sum(f.length for f in self._fileobjs)
119 return sum(f.length for f in self._fileobjs)
116
120
117 def read(self, amt=None):
121 def read(self, amt=None):
118 if amt <= 0:
122 if amt <= 0:
119 return ''.join(f.read() for f in self._fileobjs)
123 return ''.join(f.read() for f in self._fileobjs)
120 parts = []
124 parts = []
121 while amt and self._index < len(self._fileobjs):
125 while amt and self._index < len(self._fileobjs):
122 parts.append(self._fileobjs[self._index].read(amt))
126 parts.append(self._fileobjs[self._index].read(amt))
123 got = len(parts[-1])
127 got = len(parts[-1])
124 if got < amt:
128 if got < amt:
125 self._index += 1
129 self._index += 1
126 amt -= got
130 amt -= got
127 return ''.join(parts)
131 return ''.join(parts)
128
132
129 def seek(self, offset, whence=os.SEEK_SET):
133 def seek(self, offset, whence=os.SEEK_SET):
130 if whence != os.SEEK_SET:
134 if whence != os.SEEK_SET:
131 raise NotImplementedError(
135 raise NotImplementedError(
132 '_multifile does not support anything other'
136 '_multifile does not support anything other'
133 ' than os.SEEK_SET for whence on seek()')
137 ' than os.SEEK_SET for whence on seek()')
134 if offset != 0:
138 if offset != 0:
135 raise NotImplementedError(
139 raise NotImplementedError(
136 '_multifile only supports seeking to start, but that '
140 '_multifile only supports seeking to start, but that '
137 'could be fixed if you need it')
141 'could be fixed if you need it')
138 for f in self._fileobjs:
142 for f in self._fileobjs:
139 f.seek(0)
143 f.seek(0)
140 self._index = 0
144 self._index = 0
141
145
142 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
146 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
143 repobaseurl, cmd, args):
147 repobaseurl, cmd, args):
144 """Make an HTTP request to run a command for a version 1 client.
148 """Make an HTTP request to run a command for a version 1 client.
145
149
146 ``caps`` is a set of known server capabilities. The value may be
150 ``caps`` is a set of known server capabilities. The value may be
147 None if capabilities are not yet known.
151 None if capabilities are not yet known.
148
152
149 ``capablefn`` is a function to evaluate a capability.
153 ``capablefn`` is a function to evaluate a capability.
150
154
151 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
155 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
152 raw data to pass to it.
156 raw data to pass to it.
153 """
157 """
154 if cmd == 'pushkey':
158 if cmd == 'pushkey':
155 args['data'] = ''
159 args['data'] = ''
156 data = args.pop('data', None)
160 data = args.pop('data', None)
157 headers = args.pop('headers', {})
161 headers = args.pop('headers', {})
158
162
159 ui.debug("sending %s command\n" % cmd)
163 ui.debug("sending %s command\n" % cmd)
160 q = [('cmd', cmd)]
164 q = [('cmd', cmd)]
161 headersize = 0
165 headersize = 0
162 # Important: don't use self.capable() here or else you end up
166 # Important: don't use self.capable() here or else you end up
163 # with infinite recursion when trying to look up capabilities
167 # with infinite recursion when trying to look up capabilities
164 # for the first time.
168 # for the first time.
165 postargsok = caps is not None and 'httppostargs' in caps
169 postargsok = caps is not None and 'httppostargs' in caps
166
170
167 # Send arguments via POST.
171 # Send arguments via POST.
168 if postargsok and args:
172 if postargsok and args:
169 strargs = urlreq.urlencode(sorted(args.items()))
173 strargs = urlreq.urlencode(sorted(args.items()))
170 if not data:
174 if not data:
171 data = strargs
175 data = strargs
172 else:
176 else:
173 if isinstance(data, bytes):
177 if isinstance(data, bytes):
174 i = io.BytesIO(data)
178 i = io.BytesIO(data)
175 i.length = len(data)
179 i.length = len(data)
176 data = i
180 data = i
177 argsio = io.BytesIO(strargs)
181 argsio = io.BytesIO(strargs)
178 argsio.length = len(strargs)
182 argsio.length = len(strargs)
179 data = _multifile(argsio, data)
183 data = _multifile(argsio, data)
180 headers[r'X-HgArgs-Post'] = len(strargs)
184 headers[r'X-HgArgs-Post'] = len(strargs)
181 elif args:
185 elif args:
182 # Calling self.capable() can infinite loop if we are calling
186 # Calling self.capable() can infinite loop if we are calling
183 # "capabilities". But that command should never accept wire
187 # "capabilities". But that command should never accept wire
184 # protocol arguments. So this should never happen.
188 # protocol arguments. So this should never happen.
185 assert cmd != 'capabilities'
189 assert cmd != 'capabilities'
186 httpheader = capablefn('httpheader')
190 httpheader = capablefn('httpheader')
187 if httpheader:
191 if httpheader:
188 headersize = int(httpheader.split(',', 1)[0])
192 headersize = int(httpheader.split(',', 1)[0])
189
193
190 # Send arguments via HTTP headers.
194 # Send arguments via HTTP headers.
191 if headersize > 0:
195 if headersize > 0:
192 # The headers can typically carry more data than the URL.
196 # The headers can typically carry more data than the URL.
193 encargs = urlreq.urlencode(sorted(args.items()))
197 encargs = urlreq.urlencode(sorted(args.items()))
194 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
198 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
195 headersize):
199 headersize):
196 headers[header] = value
200 headers[header] = value
197 # Send arguments via query string (Mercurial <1.9).
201 # Send arguments via query string (Mercurial <1.9).
198 else:
202 else:
199 q += sorted(args.items())
203 q += sorted(args.items())
200
204
201 qs = '?%s' % urlreq.urlencode(q)
205 qs = '?%s' % urlreq.urlencode(q)
202 cu = "%s%s" % (repobaseurl, qs)
206 cu = "%s%s" % (repobaseurl, qs)
203 size = 0
207 size = 0
204 if util.safehasattr(data, 'length'):
208 if util.safehasattr(data, 'length'):
205 size = data.length
209 size = data.length
206 elif data is not None:
210 elif data is not None:
207 size = len(data)
211 size = len(data)
208 if data is not None and r'Content-Type' not in headers:
212 if data is not None and r'Content-Type' not in headers:
209 headers[r'Content-Type'] = r'application/mercurial-0.1'
213 headers[r'Content-Type'] = r'application/mercurial-0.1'
210
214
211 # Tell the server we accept application/mercurial-0.2 and multiple
215 # Tell the server we accept application/mercurial-0.2 and multiple
212 # compression formats if the server is capable of emitting those
216 # compression formats if the server is capable of emitting those
213 # payloads.
217 # payloads.
214 # Note: Keep this set empty by default, as client advertisement of
218 # Note: Keep this set empty by default, as client advertisement of
215 # protocol parameters should only occur after the handshake.
219 # protocol parameters should only occur after the handshake.
216 protoparams = set()
220 protoparams = set()
217
221
218 mediatypes = set()
222 mediatypes = set()
219 if caps is not None:
223 if caps is not None:
220 mt = capablefn('httpmediatype')
224 mt = capablefn('httpmediatype')
221 if mt:
225 if mt:
222 protoparams.add('0.1')
226 protoparams.add('0.1')
223 mediatypes = set(mt.split(','))
227 mediatypes = set(mt.split(','))
224
228
225 protoparams.add('partial-pull')
229 protoparams.add('partial-pull')
226
230
227 if '0.2tx' in mediatypes:
231 if '0.2tx' in mediatypes:
228 protoparams.add('0.2')
232 protoparams.add('0.2')
229
233
230 if '0.2tx' in mediatypes and capablefn('compression'):
234 if '0.2tx' in mediatypes and capablefn('compression'):
231 # We /could/ compare supported compression formats and prune
235 # We /could/ compare supported compression formats and prune
232 # non-mutually supported or error if nothing is mutually supported.
236 # non-mutually supported or error if nothing is mutually supported.
233 # For now, send the full list to the server and have it error.
237 # For now, send the full list to the server and have it error.
234 comps = [e.wireprotosupport().name for e in
238 comps = [e.wireprotosupport().name for e in
235 util.compengines.supportedwireengines(util.CLIENTROLE)]
239 util.compengines.supportedwireengines(util.CLIENTROLE)]
236 protoparams.add('comp=%s' % ','.join(comps))
240 protoparams.add('comp=%s' % ','.join(comps))
237
241
238 if protoparams:
242 if protoparams:
239 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
243 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
240 'X-HgProto',
244 'X-HgProto',
241 headersize or 1024)
245 headersize or 1024)
242 for header, value in protoheaders:
246 for header, value in protoheaders:
243 headers[header] = value
247 headers[header] = value
244
248
245 varyheaders = []
249 varyheaders = []
246 for header in headers:
250 for header in headers:
247 if header.lower().startswith(r'x-hg'):
251 if header.lower().startswith(r'x-hg'):
248 varyheaders.append(header)
252 varyheaders.append(header)
249
253
250 if varyheaders:
254 if varyheaders:
251 headers[r'Vary'] = r','.join(sorted(varyheaders))
255 headers[r'Vary'] = r','.join(sorted(varyheaders))
252
256
253 req = requestbuilder(pycompat.strurl(cu), data, headers)
257 req = requestbuilder(pycompat.strurl(cu), data, headers)
254
258
255 if data is not None:
259 if data is not None:
256 ui.debug("sending %d bytes\n" % size)
260 ui.debug("sending %d bytes\n" % size)
257 req.add_unredirected_header(r'Content-Length', r'%d' % size)
261 req.add_unredirected_header(r'Content-Length', r'%d' % size)
258
262
259 return req, cu, qs
263 return req, cu, qs
260
264
261 def sendrequest(ui, opener, req):
265 def sendrequest(ui, opener, req):
262 """Send a prepared HTTP request.
266 """Send a prepared HTTP request.
263
267
264 Returns the response object.
268 Returns the response object.
265 """
269 """
266 if (ui.debugflag
270 if (ui.debugflag
267 and ui.configbool('devel', 'debug.peer-request')):
271 and ui.configbool('devel', 'debug.peer-request')):
268 dbg = ui.debug
272 dbg = ui.debug
269 line = 'devel-peer-request: %s\n'
273 line = 'devel-peer-request: %s\n'
270 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
274 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
271 hgargssize = None
275 hgargssize = None
272
276
273 for header, value in sorted(req.header_items()):
277 for header, value in sorted(req.header_items()):
274 if header.startswith('X-hgarg-'):
278 if header.startswith('X-hgarg-'):
275 if hgargssize is None:
279 if hgargssize is None:
276 hgargssize = 0
280 hgargssize = 0
277 hgargssize += len(value)
281 hgargssize += len(value)
278 else:
282 else:
279 dbg(line % ' %s %s' % (header, value))
283 dbg(line % ' %s %s' % (header, value))
280
284
281 if hgargssize is not None:
285 if hgargssize is not None:
282 dbg(line % ' %d bytes of commands arguments in headers'
286 dbg(line % ' %d bytes of commands arguments in headers'
283 % hgargssize)
287 % hgargssize)
284
288
285 if req.has_data():
289 if req.has_data():
286 data = req.get_data()
290 data = req.get_data()
287 length = getattr(data, 'length', None)
291 length = getattr(data, 'length', None)
288 if length is None:
292 if length is None:
289 length = len(data)
293 length = len(data)
290 dbg(line % ' %d bytes of data' % length)
294 dbg(line % ' %d bytes of data' % length)
291
295
292 start = util.timer()
296 start = util.timer()
293
297
294 try:
298 try:
295 res = opener.open(req)
299 res = opener.open(req)
296 except urlerr.httperror as inst:
300 except urlerr.httperror as inst:
297 if inst.code == 401:
301 if inst.code == 401:
298 raise error.Abort(_('authorization failed'))
302 raise error.Abort(_('authorization failed'))
299 raise
303 raise
300 except httplib.HTTPException as inst:
304 except httplib.HTTPException as inst:
301 ui.debug('http error requesting %s\n' %
305 ui.debug('http error requesting %s\n' %
302 util.hidepassword(req.get_full_url()))
306 util.hidepassword(req.get_full_url()))
303 ui.traceback()
307 ui.traceback()
304 raise IOError(None, inst)
308 raise IOError(None, inst)
305 finally:
309 finally:
306 if ui.configbool('devel', 'debug.peer-request'):
310 if ui.configbool('devel', 'debug.peer-request'):
307 dbg(line % ' finished in %.4f seconds (%s)'
311 dbg(line % ' finished in %.4f seconds (%s)'
308 % (util.timer() - start, res.code))
312 % (util.timer() - start, res.code))
309
313
310 # Insert error handlers for common I/O failures.
314 # Insert error handlers for common I/O failures.
311 _wraphttpresponse(res)
315 _wraphttpresponse(res)
312
316
313 return res
317 return res
314
318
315 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
319 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
316 allowcbor=False):
320 allowcbor=False):
317 # record the url we got redirected to
321 # record the url we got redirected to
318 respurl = pycompat.bytesurl(resp.geturl())
322 respurl = pycompat.bytesurl(resp.geturl())
319 if respurl.endswith(qs):
323 if respurl.endswith(qs):
320 respurl = respurl[:-len(qs)]
324 respurl = respurl[:-len(qs)]
321 if baseurl.rstrip('/') != respurl.rstrip('/'):
325 if baseurl.rstrip('/') != respurl.rstrip('/'):
322 if not ui.quiet:
326 if not ui.quiet:
323 ui.warn(_('real URL is %s\n') % respurl)
327 ui.warn(_('real URL is %s\n') % respurl)
324
328
325 try:
329 try:
326 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
330 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
327 except AttributeError:
331 except AttributeError:
328 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
332 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
329
333
330 safeurl = util.hidepassword(baseurl)
334 safeurl = util.hidepassword(baseurl)
331 if proto.startswith('application/hg-error'):
335 if proto.startswith('application/hg-error'):
332 raise error.OutOfBandError(resp.read())
336 raise error.OutOfBandError(resp.read())
333
337
334 # Pre 1.0 versions of Mercurial used text/plain and
338 # Pre 1.0 versions of Mercurial used text/plain and
335 # application/hg-changegroup. We don't support such old servers.
339 # application/hg-changegroup. We don't support such old servers.
336 if not proto.startswith('application/mercurial-'):
340 if not proto.startswith('application/mercurial-'):
337 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
341 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
338 raise error.RepoError(
342 raise error.RepoError(
339 _("'%s' does not appear to be an hg repository:\n"
343 _("'%s' does not appear to be an hg repository:\n"
340 "---%%<--- (%s)\n%s\n---%%<---\n")
344 "---%%<--- (%s)\n%s\n---%%<---\n")
341 % (safeurl, proto or 'no content-type', resp.read(1024)))
345 % (safeurl, proto or 'no content-type', resp.read(1024)))
342
346
343 try:
347 try:
344 subtype = proto.split('-', 1)[1]
348 subtype = proto.split('-', 1)[1]
345
349
346 # Unless we end up supporting CBOR in the legacy wire protocol,
350 # Unless we end up supporting CBOR in the legacy wire protocol,
347 # this should ONLY be encountered for the initial capabilities
351 # this should ONLY be encountered for the initial capabilities
348 # request during handshake.
352 # request during handshake.
349 if subtype == 'cbor':
353 if subtype == 'cbor':
350 if allowcbor:
354 if allowcbor:
351 return respurl, proto, resp
355 return respurl, proto, resp
352 else:
356 else:
353 raise error.RepoError(_('unexpected CBOR response from '
357 raise error.RepoError(_('unexpected CBOR response from '
354 'server'))
358 'server'))
355
359
356 version_info = tuple([int(n) for n in subtype.split('.')])
360 version_info = tuple([int(n) for n in subtype.split('.')])
357 except ValueError:
361 except ValueError:
358 raise error.RepoError(_("'%s' sent a broken Content-Type "
362 raise error.RepoError(_("'%s' sent a broken Content-Type "
359 "header (%s)") % (safeurl, proto))
363 "header (%s)") % (safeurl, proto))
360
364
361 # TODO consider switching to a decompression reader that uses
365 # TODO consider switching to a decompression reader that uses
362 # generators.
366 # generators.
363 if version_info == (0, 1):
367 if version_info == (0, 1):
364 if compressible:
368 if compressible:
365 resp = util.compengines['zlib'].decompressorreader(resp)
369 resp = util.compengines['zlib'].decompressorreader(resp)
366
370
367 elif version_info == (0, 2):
371 elif version_info == (0, 2):
368 # application/mercurial-0.2 always identifies the compression
372 # application/mercurial-0.2 always identifies the compression
369 # engine in the payload header.
373 # engine in the payload header.
370 elen = struct.unpack('B', resp.read(1))[0]
374 elen = struct.unpack('B', resp.read(1))[0]
371 ename = resp.read(elen)
375 ename = resp.read(elen)
372 engine = util.compengines.forwiretype(ename)
376 engine = util.compengines.forwiretype(ename)
373
377
374 resp = engine.decompressorreader(resp)
378 resp = engine.decompressorreader(resp)
375 else:
379 else:
376 raise error.RepoError(_("'%s' uses newer protocol %s") %
380 raise error.RepoError(_("'%s' uses newer protocol %s") %
377 (safeurl, subtype))
381 (safeurl, subtype))
378
382
379 return respurl, proto, resp
383 return respurl, proto, resp
380
384
381 class httppeer(wireproto.wirepeer):
385 class httppeer(wireproto.wirepeer):
382 def __init__(self, ui, path, url, opener, requestbuilder, caps):
386 def __init__(self, ui, path, url, opener, requestbuilder, caps):
383 self.ui = ui
387 self.ui = ui
384 self._path = path
388 self._path = path
385 self._url = url
389 self._url = url
386 self._caps = caps
390 self._caps = caps
387 self._urlopener = opener
391 self._urlopener = opener
388 self._requestbuilder = requestbuilder
392 self._requestbuilder = requestbuilder
389
393
390 def __del__(self):
394 def __del__(self):
391 for h in self._urlopener.handlers:
395 for h in self._urlopener.handlers:
392 h.close()
396 h.close()
393 getattr(h, "close_all", lambda: None)()
397 getattr(h, "close_all", lambda: None)()
394
398
395 # Begin of ipeerconnection interface.
399 # Begin of ipeerconnection interface.
396
400
397 def url(self):
401 def url(self):
398 return self._path
402 return self._path
399
403
400 def local(self):
404 def local(self):
401 return None
405 return None
402
406
403 def peer(self):
407 def peer(self):
404 return self
408 return self
405
409
406 def canpush(self):
410 def canpush(self):
407 return True
411 return True
408
412
409 def close(self):
413 def close(self):
410 pass
414 pass
411
415
412 # End of ipeerconnection interface.
416 # End of ipeerconnection interface.
413
417
414 # Begin of ipeercommands interface.
418 # Begin of ipeercommands interface.
415
419
416 def capabilities(self):
420 def capabilities(self):
417 return self._caps
421 return self._caps
418
422
419 # End of ipeercommands interface.
423 # End of ipeercommands interface.
420
424
421 # look up capabilities only when needed
425 # look up capabilities only when needed
422
426
423 def _callstream(self, cmd, _compressible=False, **args):
427 def _callstream(self, cmd, _compressible=False, **args):
424 args = pycompat.byteskwargs(args)
428 args = pycompat.byteskwargs(args)
425
429
426 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
430 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
427 self._caps, self.capable,
431 self._caps, self.capable,
428 self._url, cmd, args)
432 self._url, cmd, args)
429
433
430 resp = sendrequest(self.ui, self._urlopener, req)
434 resp = sendrequest(self.ui, self._urlopener, req)
431
435
432 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
436 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
433 resp, _compressible)
437 resp, _compressible)
434
438
435 return resp
439 return resp
436
440
437 def _call(self, cmd, **args):
441 def _call(self, cmd, **args):
438 fp = self._callstream(cmd, **args)
442 fp = self._callstream(cmd, **args)
439 try:
443 try:
440 return fp.read()
444 return fp.read()
441 finally:
445 finally:
442 # if using keepalive, allow connection to be reused
446 # if using keepalive, allow connection to be reused
443 fp.close()
447 fp.close()
444
448
445 def _callpush(self, cmd, cg, **args):
449 def _callpush(self, cmd, cg, **args):
446 # have to stream bundle to a temp file because we do not have
450 # have to stream bundle to a temp file because we do not have
447 # http 1.1 chunked transfer.
451 # http 1.1 chunked transfer.
448
452
449 types = self.capable('unbundle')
453 types = self.capable('unbundle')
450 try:
454 try:
451 types = types.split(',')
455 types = types.split(',')
452 except AttributeError:
456 except AttributeError:
453 # servers older than d1b16a746db6 will send 'unbundle' as a
457 # servers older than d1b16a746db6 will send 'unbundle' as a
454 # boolean capability. They only support headerless/uncompressed
458 # boolean capability. They only support headerless/uncompressed
455 # bundles.
459 # bundles.
456 types = [""]
460 types = [""]
457 for x in types:
461 for x in types:
458 if x in bundle2.bundletypes:
462 if x in bundle2.bundletypes:
459 type = x
463 type = x
460 break
464 break
461
465
462 tempname = bundle2.writebundle(self.ui, cg, None, type)
466 tempname = bundle2.writebundle(self.ui, cg, None, type)
463 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
467 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
464 headers = {r'Content-Type': r'application/mercurial-0.1'}
468 headers = {r'Content-Type': r'application/mercurial-0.1'}
465
469
466 try:
470 try:
467 r = self._call(cmd, data=fp, headers=headers, **args)
471 r = self._call(cmd, data=fp, headers=headers, **args)
468 vals = r.split('\n', 1)
472 vals = r.split('\n', 1)
469 if len(vals) < 2:
473 if len(vals) < 2:
470 raise error.ResponseError(_("unexpected response:"), r)
474 raise error.ResponseError(_("unexpected response:"), r)
471 return vals
475 return vals
472 except urlerr.httperror:
476 except urlerr.httperror:
473 # Catch and re-raise these so we don't try and treat them
477 # Catch and re-raise these so we don't try and treat them
474 # like generic socket errors. They lack any values in
478 # like generic socket errors. They lack any values in
475 # .args on Python 3 which breaks our socket.error block.
479 # .args on Python 3 which breaks our socket.error block.
476 raise
480 raise
477 except socket.error as err:
481 except socket.error as err:
478 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
482 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
479 raise error.Abort(_('push failed: %s') % err.args[1])
483 raise error.Abort(_('push failed: %s') % err.args[1])
480 raise error.Abort(err.args[1])
484 raise error.Abort(err.args[1])
481 finally:
485 finally:
482 fp.close()
486 fp.close()
483 os.unlink(tempname)
487 os.unlink(tempname)
484
488
485 def _calltwowaystream(self, cmd, fp, **args):
489 def _calltwowaystream(self, cmd, fp, **args):
486 fh = None
490 fh = None
487 fp_ = None
491 fp_ = None
488 filename = None
492 filename = None
489 try:
493 try:
490 # dump bundle to disk
494 # dump bundle to disk
491 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
495 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
492 fh = os.fdopen(fd, r"wb")
496 fh = os.fdopen(fd, r"wb")
493 d = fp.read(4096)
497 d = fp.read(4096)
494 while d:
498 while d:
495 fh.write(d)
499 fh.write(d)
496 d = fp.read(4096)
500 d = fp.read(4096)
497 fh.close()
501 fh.close()
498 # start http push
502 # start http push
499 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
503 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
500 headers = {r'Content-Type': r'application/mercurial-0.1'}
504 headers = {r'Content-Type': r'application/mercurial-0.1'}
501 return self._callstream(cmd, data=fp_, headers=headers, **args)
505 return self._callstream(cmd, data=fp_, headers=headers, **args)
502 finally:
506 finally:
503 if fp_ is not None:
507 if fp_ is not None:
504 fp_.close()
508 fp_.close()
505 if fh is not None:
509 if fh is not None:
506 fh.close()
510 fh.close()
507 os.unlink(filename)
511 os.unlink(filename)
508
512
509 def _callcompressable(self, cmd, **args):
513 def _callcompressable(self, cmd, **args):
510 return self._callstream(cmd, _compressible=True, **args)
514 return self._callstream(cmd, _compressible=True, **args)
511
515
512 def _abort(self, exception):
516 def _abort(self, exception):
513 raise exception
517 raise exception
514
518
515 # TODO implement interface for version 2 peers
519 # TODO implement interface for version 2 peers
520 @zi.implementer(repository.ipeerconnection)
516 class httpv2peer(object):
521 class httpv2peer(object):
517 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
522 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
518 apidescriptor):
523 apidescriptor):
519 self.ui = ui
524 self.ui = ui
520
525
521 if repourl.endswith('/'):
526 if repourl.endswith('/'):
522 repourl = repourl[:-1]
527 repourl = repourl[:-1]
523
528
524 self.url = repourl
529 self._url = repourl
525 self._apipath = apipath
530 self._apipath = apipath
526 self._opener = opener
531 self._opener = opener
527 self._requestbuilder = requestbuilder
532 self._requestbuilder = requestbuilder
528 self._descriptor = apidescriptor
533 self._descriptor = apidescriptor
529
534
535 # Start of ipeerconnection.
536
537 def url(self):
538 return self._url
539
540 def local(self):
541 return None
542
543 def peer(self):
544 return self
545
546 def canpush(self):
547 # TODO change once implemented.
548 return False
549
530 def close(self):
550 def close(self):
531 pass
551 pass
532
552
553 # End of ipeerconnection.
554
533 # TODO require to be part of a batched primitive, use futures.
555 # TODO require to be part of a batched primitive, use futures.
534 def _call(self, name, **args):
556 def _call(self, name, **args):
535 """Call a wire protocol command with arguments."""
557 """Call a wire protocol command with arguments."""
536
558
537 # Having this early has a side-effect of importing wireprotov2server,
559 # Having this early has a side-effect of importing wireprotov2server,
538 # which has the side-effect of ensuring commands are registered.
560 # which has the side-effect of ensuring commands are registered.
539
561
540 # TODO modify user-agent to reflect v2.
562 # TODO modify user-agent to reflect v2.
541 headers = {
563 headers = {
542 r'Accept': wireprotov2server.FRAMINGTYPE,
564 r'Accept': wireprotov2server.FRAMINGTYPE,
543 r'Content-Type': wireprotov2server.FRAMINGTYPE,
565 r'Content-Type': wireprotov2server.FRAMINGTYPE,
544 }
566 }
545
567
546 # TODO permissions should come from capabilities results.
568 # TODO permissions should come from capabilities results.
547 permission = wireproto.commandsv2[name].permission
569 permission = wireproto.commandsv2[name].permission
548 if permission not in ('push', 'pull'):
570 if permission not in ('push', 'pull'):
549 raise error.ProgrammingError('unknown permission type: %s' %
571 raise error.ProgrammingError('unknown permission type: %s' %
550 permission)
572 permission)
551
573
552 permission = {
574 permission = {
553 'push': 'rw',
575 'push': 'rw',
554 'pull': 'ro',
576 'pull': 'ro',
555 }[permission]
577 }[permission]
556
578
557 url = '%s/%s/%s/%s' % (self.url, self._apipath, permission, name)
579 url = '%s/%s/%s/%s' % (self._url, self._apipath, permission, name)
558
580
559 # TODO this should be part of a generic peer for the frame-based
581 # TODO this should be part of a generic peer for the frame-based
560 # protocol.
582 # protocol.
561 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
583 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
562 buffersends=True)
584 buffersends=True)
563
585
564 request, action, meta = reactor.callcommand(name, args)
586 request, action, meta = reactor.callcommand(name, args)
565 assert action == 'noop'
587 assert action == 'noop'
566
588
567 action, meta = reactor.flushcommands()
589 action, meta = reactor.flushcommands()
568 assert action == 'sendframes'
590 assert action == 'sendframes'
569
591
570 body = b''.join(map(bytes, meta['framegen']))
592 body = b''.join(map(bytes, meta['framegen']))
571 req = self._requestbuilder(pycompat.strurl(url), body, headers)
593 req = self._requestbuilder(pycompat.strurl(url), body, headers)
572 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
594 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
573
595
574 # TODO unify this code with httppeer.
596 # TODO unify this code with httppeer.
575 try:
597 try:
576 res = self._opener.open(req)
598 res = self._opener.open(req)
577 except urlerr.httperror as e:
599 except urlerr.httperror as e:
578 if e.code == 401:
600 if e.code == 401:
579 raise error.Abort(_('authorization failed'))
601 raise error.Abort(_('authorization failed'))
580
602
581 raise
603 raise
582 except httplib.HTTPException as e:
604 except httplib.HTTPException as e:
583 self.ui.traceback()
605 self.ui.traceback()
584 raise IOError(None, e)
606 raise IOError(None, e)
585
607
586 # TODO validate response type, wrap response to handle I/O errors.
608 # TODO validate response type, wrap response to handle I/O errors.
587 # TODO more robust frame receiver.
609 # TODO more robust frame receiver.
588 results = []
610 results = []
589
611
590 while True:
612 while True:
591 frame = wireprotoframing.readframe(res)
613 frame = wireprotoframing.readframe(res)
592 if frame is None:
614 if frame is None:
593 break
615 break
594
616
595 self.ui.note(_('received %r\n') % frame)
617 self.ui.note(_('received %r\n') % frame)
596
618
597 action, meta = reactor.onframerecv(frame)
619 action, meta = reactor.onframerecv(frame)
598
620
599 if action == 'responsedata':
621 if action == 'responsedata':
600 if meta['cbor']:
622 if meta['cbor']:
601 payload = util.bytesio(meta['data'])
623 payload = util.bytesio(meta['data'])
602
624
603 decoder = cbor.CBORDecoder(payload)
625 decoder = cbor.CBORDecoder(payload)
604 while payload.tell() + 1 < len(meta['data']):
626 while payload.tell() + 1 < len(meta['data']):
605 results.append(decoder.decode())
627 results.append(decoder.decode())
606 else:
628 else:
607 results.append(meta['data'])
629 results.append(meta['data'])
608 else:
630 else:
609 error.ProgrammingError('unhandled action: %s' % action)
631 error.ProgrammingError('unhandled action: %s' % action)
610
632
611 return results
633 return results
612
634
613 # Registry of API service names to metadata about peers that handle it.
635 # Registry of API service names to metadata about peers that handle it.
614 #
636 #
615 # The following keys are meaningful:
637 # The following keys are meaningful:
616 #
638 #
617 # init
639 # init
618 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
640 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
619 # apidescriptor) to create a peer.
641 # apidescriptor) to create a peer.
620 #
642 #
621 # priority
643 # priority
622 # Integer priority for the service. If we could choose from multiple
644 # Integer priority for the service. If we could choose from multiple
623 # services, we choose the one with the highest priority.
645 # services, we choose the one with the highest priority.
624 API_PEERS = {
646 API_PEERS = {
625 wireprototypes.HTTPV2: {
647 wireprototypes.HTTPV2: {
626 'init': httpv2peer,
648 'init': httpv2peer,
627 'priority': 50,
649 'priority': 50,
628 },
650 },
629 }
651 }
630
652
631 def performhandshake(ui, url, opener, requestbuilder):
653 def performhandshake(ui, url, opener, requestbuilder):
632 # The handshake is a request to the capabilities command.
654 # The handshake is a request to the capabilities command.
633
655
634 caps = None
656 caps = None
635 def capable(x):
657 def capable(x):
636 raise error.ProgrammingError('should not be called')
658 raise error.ProgrammingError('should not be called')
637
659
638 args = {}
660 args = {}
639
661
640 # The client advertises support for newer protocols by adding an
662 # The client advertises support for newer protocols by adding an
641 # X-HgUpgrade-* header with a list of supported APIs and an
663 # X-HgUpgrade-* header with a list of supported APIs and an
642 # X-HgProto-* header advertising which serializing formats it supports.
664 # X-HgProto-* header advertising which serializing formats it supports.
643 # We only support the HTTP version 2 transport and CBOR responses for
665 # We only support the HTTP version 2 transport and CBOR responses for
644 # now.
666 # now.
645 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
667 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
646
668
647 if advertisev2:
669 if advertisev2:
648 args['headers'] = {
670 args['headers'] = {
649 r'X-HgProto-1': r'cbor',
671 r'X-HgProto-1': r'cbor',
650 }
672 }
651
673
652 args['headers'].update(
674 args['headers'].update(
653 encodevalueinheaders(' '.join(sorted(API_PEERS)),
675 encodevalueinheaders(' '.join(sorted(API_PEERS)),
654 'X-HgUpgrade',
676 'X-HgUpgrade',
655 # We don't know the header limit this early.
677 # We don't know the header limit this early.
656 # So make it small.
678 # So make it small.
657 1024))
679 1024))
658
680
659 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
681 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
660 capable, url, 'capabilities',
682 capable, url, 'capabilities',
661 args)
683 args)
662
684
663 resp = sendrequest(ui, opener, req)
685 resp = sendrequest(ui, opener, req)
664
686
665 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
687 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
666 compressible=False,
688 compressible=False,
667 allowcbor=advertisev2)
689 allowcbor=advertisev2)
668
690
669 try:
691 try:
670 rawdata = resp.read()
692 rawdata = resp.read()
671 finally:
693 finally:
672 resp.close()
694 resp.close()
673
695
674 if not ct.startswith('application/mercurial-'):
696 if not ct.startswith('application/mercurial-'):
675 raise error.ProgrammingError('unexpected content-type: %s' % ct)
697 raise error.ProgrammingError('unexpected content-type: %s' % ct)
676
698
677 if advertisev2:
699 if advertisev2:
678 if ct == 'application/mercurial-cbor':
700 if ct == 'application/mercurial-cbor':
679 try:
701 try:
680 info = cbor.loads(rawdata)
702 info = cbor.loads(rawdata)
681 except cbor.CBORDecodeError:
703 except cbor.CBORDecodeError:
682 raise error.Abort(_('error decoding CBOR from remote server'),
704 raise error.Abort(_('error decoding CBOR from remote server'),
683 hint=_('try again and consider contacting '
705 hint=_('try again and consider contacting '
684 'the server operator'))
706 'the server operator'))
685
707
686 # We got a legacy response. That's fine.
708 # We got a legacy response. That's fine.
687 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
709 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
688 info = {
710 info = {
689 'v1capabilities': set(rawdata.split())
711 'v1capabilities': set(rawdata.split())
690 }
712 }
691
713
692 else:
714 else:
693 raise error.RepoError(
715 raise error.RepoError(
694 _('unexpected response type from server: %s') % ct)
716 _('unexpected response type from server: %s') % ct)
695 else:
717 else:
696 info = {
718 info = {
697 'v1capabilities': set(rawdata.split())
719 'v1capabilities': set(rawdata.split())
698 }
720 }
699
721
700 return respurl, info
722 return respurl, info
701
723
702 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
724 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
703 """Construct an appropriate HTTP peer instance.
725 """Construct an appropriate HTTP peer instance.
704
726
705 ``opener`` is an ``url.opener`` that should be used to establish
727 ``opener`` is an ``url.opener`` that should be used to establish
706 connections, perform HTTP requests.
728 connections, perform HTTP requests.
707
729
708 ``requestbuilder`` is the type used for constructing HTTP requests.
730 ``requestbuilder`` is the type used for constructing HTTP requests.
709 It exists as an argument so extensions can override the default.
731 It exists as an argument so extensions can override the default.
710 """
732 """
711 u = util.url(path)
733 u = util.url(path)
712 if u.query or u.fragment:
734 if u.query or u.fragment:
713 raise error.Abort(_('unsupported URL component: "%s"') %
735 raise error.Abort(_('unsupported URL component: "%s"') %
714 (u.query or u.fragment))
736 (u.query or u.fragment))
715
737
716 # urllib cannot handle URLs with embedded user or passwd.
738 # urllib cannot handle URLs with embedded user or passwd.
717 url, authinfo = u.authinfo()
739 url, authinfo = u.authinfo()
718 ui.debug('using %s\n' % url)
740 ui.debug('using %s\n' % url)
719
741
720 opener = opener or urlmod.opener(ui, authinfo)
742 opener = opener or urlmod.opener(ui, authinfo)
721
743
722 respurl, info = performhandshake(ui, url, opener, requestbuilder)
744 respurl, info = performhandshake(ui, url, opener, requestbuilder)
723
745
724 # Given the intersection of APIs that both we and the server support,
746 # Given the intersection of APIs that both we and the server support,
725 # sort by their advertised priority and pick the first one.
747 # sort by their advertised priority and pick the first one.
726 #
748 #
727 # TODO consider making this request-based and interface driven. For
749 # TODO consider making this request-based and interface driven. For
728 # example, the caller could say "I want a peer that does X." It's quite
750 # example, the caller could say "I want a peer that does X." It's quite
729 # possible that not all peers would do that. Since we know the service
751 # possible that not all peers would do that. Since we know the service
730 # capabilities, we could filter out services not meeting the
752 # capabilities, we could filter out services not meeting the
731 # requirements. Possibly by consulting the interfaces defined by the
753 # requirements. Possibly by consulting the interfaces defined by the
732 # peer type.
754 # peer type.
733 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
755 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
734
756
735 preferredchoices = sorted(apipeerchoices,
757 preferredchoices = sorted(apipeerchoices,
736 key=lambda x: API_PEERS[x]['priority'],
758 key=lambda x: API_PEERS[x]['priority'],
737 reverse=True)
759 reverse=True)
738
760
739 for service in preferredchoices:
761 for service in preferredchoices:
740 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
762 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
741
763
742 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
764 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
743 requestbuilder,
765 requestbuilder,
744 info['apis'][service])
766 info['apis'][service])
745
767
746 # Failed to construct an API peer. Fall back to legacy.
768 # Failed to construct an API peer. Fall back to legacy.
747 return httppeer(ui, path, respurl, opener, requestbuilder,
769 return httppeer(ui, path, respurl, opener, requestbuilder,
748 info['v1capabilities'])
770 info['v1capabilities'])
749
771
750 def instance(ui, path, create):
772 def instance(ui, path, create):
751 if create:
773 if create:
752 raise error.Abort(_('cannot create new http repository'))
774 raise error.Abort(_('cannot create new http repository'))
753 try:
775 try:
754 if path.startswith('https:') and not urlmod.has_https:
776 if path.startswith('https:') and not urlmod.has_https:
755 raise error.Abort(_('Python support for SSL and HTTPS '
777 raise error.Abort(_('Python support for SSL and HTTPS '
756 'is not installed'))
778 'is not installed'))
757
779
758 inst = makepeer(ui, path)
780 inst = makepeer(ui, path)
759
781
760 return inst
782 return inst
761 except error.RepoError as httpexception:
783 except error.RepoError as httpexception:
762 try:
784 try:
763 r = statichttprepo.instance(ui, "static-" + path, create)
785 r = statichttprepo.instance(ui, "static-" + path, create)
764 ui.note(_('(falling back to static-http)\n'))
786 ui.note(_('(falling back to static-http)\n'))
765 return r
787 return r
766 except error.RepoError:
788 except error.RepoError:
767 raise httpexception # use the original http RepoError instead
789 raise httpexception # use the original http RepoError instead
@@ -1,148 +1,152 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 filelog,
15 filelog,
16 httppeer,
16 httppeer,
17 localrepo,
17 localrepo,
18 repository,
18 repository,
19 sshpeer,
19 sshpeer,
20 statichttprepo,
20 statichttprepo,
21 ui as uimod,
21 ui as uimod,
22 unionrepo,
22 unionrepo,
23 vfs as vfsmod,
23 vfs as vfsmod,
24 wireprotoserver,
24 wireprotoserver,
25 wireprototypes,
25 wireprototypes,
26 wireprotov2server,
26 wireprotov2server,
27 )
27 )
28
28
29 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
29 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
30
30
31 def checkzobject(o, allowextra=False):
31 def checkzobject(o, allowextra=False):
32 """Verify an object with a zope interface."""
32 """Verify an object with a zope interface."""
33 ifaces = zi.providedBy(o)
33 ifaces = zi.providedBy(o)
34 if not ifaces:
34 if not ifaces:
35 print('%r does not provide any zope interfaces' % o)
35 print('%r does not provide any zope interfaces' % o)
36 return
36 return
37
37
38 # Run zope.interface's built-in verification routine. This verifies that
38 # Run zope.interface's built-in verification routine. This verifies that
39 # everything that is supposed to be present is present.
39 # everything that is supposed to be present is present.
40 for iface in ifaces:
40 for iface in ifaces:
41 ziverify.verifyObject(iface, o)
41 ziverify.verifyObject(iface, o)
42
42
43 if allowextra:
43 if allowextra:
44 return
44 return
45
45
46 # Now verify that the object provides no extra public attributes that
46 # Now verify that the object provides no extra public attributes that
47 # aren't declared as part of interfaces.
47 # aren't declared as part of interfaces.
48 allowed = set()
48 allowed = set()
49 for iface in ifaces:
49 for iface in ifaces:
50 allowed |= set(iface.names(all=True))
50 allowed |= set(iface.names(all=True))
51
51
52 public = {a for a in dir(o) if not a.startswith('_')}
52 public = {a for a in dir(o) if not a.startswith('_')}
53
53
54 for attr in sorted(public - allowed):
54 for attr in sorted(public - allowed):
55 print('public attribute not declared in interfaces: %s.%s' % (
55 print('public attribute not declared in interfaces: %s.%s' % (
56 o.__class__.__name__, attr))
56 o.__class__.__name__, attr))
57
57
58 # Facilitates testing localpeer.
58 # Facilitates testing localpeer.
59 class dummyrepo(object):
59 class dummyrepo(object):
60 def __init__(self):
60 def __init__(self):
61 self.ui = uimod.ui()
61 self.ui = uimod.ui()
62 def filtered(self, name):
62 def filtered(self, name):
63 pass
63 pass
64 def _restrictcapabilities(self, caps):
64 def _restrictcapabilities(self, caps):
65 pass
65 pass
66
66
67 class dummyopener(object):
67 class dummyopener(object):
68 handlers = []
68 handlers = []
69
69
70 # Facilitates testing sshpeer without requiring a server.
70 # Facilitates testing sshpeer without requiring a server.
71 class badpeer(httppeer.httppeer):
71 class badpeer(httppeer.httppeer):
72 def __init__(self):
72 def __init__(self):
73 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
73 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
74 None)
74 None)
75 self.badattribute = True
75 self.badattribute = True
76
76
77 def badmethod(self):
77 def badmethod(self):
78 pass
78 pass
79
79
80 class dummypipe(object):
80 class dummypipe(object):
81 def close(self):
81 def close(self):
82 pass
82 pass
83
83
84 def main():
84 def main():
85 ui = uimod.ui()
85 ui = uimod.ui()
86 # Needed so we can open a local repo with obsstore without a warning.
86 # Needed so we can open a local repo with obsstore without a warning.
87 ui.setconfig('experimental', 'evolution.createmarkers', True)
87 ui.setconfig('experimental', 'evolution.createmarkers', True)
88
88
89 checkzobject(badpeer())
89 checkzobject(badpeer())
90
90
91 ziverify.verifyClass(repository.ipeerbaselegacycommands,
91 ziverify.verifyClass(repository.ipeerbaselegacycommands,
92 httppeer.httppeer)
92 httppeer.httppeer)
93 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
93 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
94
94
95 ziverify.verifyClass(repository.ipeerconnection,
96 httppeer.httpv2peer)
97 checkzobject(httppeer.httpv2peer(None, '', None, None, None, None))
98
95 ziverify.verifyClass(repository.ipeerbase,
99 ziverify.verifyClass(repository.ipeerbase,
96 localrepo.localpeer)
100 localrepo.localpeer)
97 checkzobject(localrepo.localpeer(dummyrepo()))
101 checkzobject(localrepo.localpeer(dummyrepo()))
98
102
99 ziverify.verifyClass(repository.ipeerbaselegacycommands,
103 ziverify.verifyClass(repository.ipeerbaselegacycommands,
100 sshpeer.sshv1peer)
104 sshpeer.sshv1peer)
101 checkzobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
105 checkzobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
102 dummypipe(), None, None))
106 dummypipe(), None, None))
103
107
104 ziverify.verifyClass(repository.ipeerbaselegacycommands,
108 ziverify.verifyClass(repository.ipeerbaselegacycommands,
105 sshpeer.sshv2peer)
109 sshpeer.sshv2peer)
106 checkzobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
110 checkzobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
107 dummypipe(), None, None))
111 dummypipe(), None, None))
108
112
109 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
113 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
110 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
114 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
111
115
112 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
116 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
113 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
117 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
114
118
115 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
119 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
116 checkzobject(unionrepo.unionpeer(dummyrepo()))
120 checkzobject(unionrepo.unionpeer(dummyrepo()))
117
121
118 ziverify.verifyClass(repository.completelocalrepository,
122 ziverify.verifyClass(repository.completelocalrepository,
119 localrepo.localrepository)
123 localrepo.localrepository)
120 repo = localrepo.localrepository(ui, rootdir)
124 repo = localrepo.localrepository(ui, rootdir)
121 checkzobject(repo)
125 checkzobject(repo)
122
126
123 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
127 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
124 wireprotoserver.sshv1protocolhandler)
128 wireprotoserver.sshv1protocolhandler)
125 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
129 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
126 wireprotoserver.sshv2protocolhandler)
130 wireprotoserver.sshv2protocolhandler)
127 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
131 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
128 wireprotoserver.httpv1protocolhandler)
132 wireprotoserver.httpv1protocolhandler)
129 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
133 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
130 wireprotov2server.httpv2protocolhandler)
134 wireprotov2server.httpv2protocolhandler)
131
135
132 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
136 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
133 checkzobject(sshv1)
137 checkzobject(sshv1)
134 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
138 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
135 checkzobject(sshv2)
139 checkzobject(sshv2)
136
140
137 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
141 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
138 checkzobject(httpv1)
142 checkzobject(httpv1)
139 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
143 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
140 checkzobject(httpv2)
144 checkzobject(httpv2)
141
145
142 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
146 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
143
147
144 vfs = vfsmod.vfs('.')
148 vfs = vfsmod.vfs('.')
145 fl = filelog.filelog(vfs, 'dummy.i')
149 fl = filelog.filelog(vfs, 'dummy.i')
146 checkzobject(fl, allowextra=True)
150 checkzobject(fl, allowextra=True)
147
151
148 main()
152 main()
General Comments 0
You need to be logged in to leave comments. Login now