##// END OF EJS Templates
exchange: refactor APIs to obtain bundle data (API)...
exchange: refactor APIs to obtain bundle data (API) Currently, exchange.getbundle() returns either a cg1unpacker or a util.chunkbuffer (in the case of bundle2). This is kinda OK, as both expose a .read() to consumers. However, localpeer.getbundle() has code inferring what the response type is based on arguments and converts the util.chunkbuffer returned in the bundle2 case to a bundle2.unbundle20 instance. This is a sign that the API for exchange.getbundle() is not ideal because it doesn't consistently return an "unbundler" instance. In addition, unbundlers mask the fact that there is an underlying generator of changegroup data. In both cg1 and bundle2, this generator is being fed into a util.chunkbuffer so it can be re-exposed as a file object. util.chunkbuffer is a nice abstraction. However, it should only be used "at the edges." This is because keeping data as a generator is more efficient than converting it to a chunkbuffer, especially if we convert that chunkbuffer back to a generator (as is the case in some code paths currently). This patch refactors exchange.getbundle() into exchange.getbundlechunks(). The new API returns an iterator of chunks instead of a file-like object. Callers of exchange.getbundle() have been updated to use the new API. There is a minor change of behavior in test-getbundle.t. This is because `hg debuggetbundle` isn't defining bundlecaps. As a result, a cg1 data stream and unpacker is being produced. This is getting fed into a new bundle20 instance via bundle2.writebundle(), which uses a backchannel mechanism between changegroup generation to add the "nbchanges" part parameter. I never liked this backchannel mechanism and I plan to remove it someday. `hg bundle` still produces the "nbchanges" part parameter, so there should be no user-visible change of behavior. I consider this "regression" a bug in `hg debuggetbundle`. And that bug is captured by an existing "TODO" in the code to use bundle2 capabilities.

File last commit:

r29455:0c741fd6 default
r30187:3e86261b default
Show More
httppeer.py
308 lines | 11.0 KiB | text/x-python | PythonLexer
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 # httppeer.py - HTTP repository proxy classes for mercurial
#
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
Gregory Szorc
httppeer: use absolute_import
r25954 from __future__ import absolute_import
import errno
import os
import socket
Pierre-Yves David
httppeer: support for _calltwowaystream...
r21074 import tempfile
Gregory Szorc
httppeer: use absolute_import
r25954 import zlib
from .i18n import _
from .node import nullid
from . import (
Martin von Zweigbergk
bundle: move writebundle() from changegroup.py to bundle2.py (API)...
r28666 bundle2,
Gregory Szorc
httppeer: use absolute_import
r25954 error,
httpconnection,
statichttprepo,
url,
util,
wireproto,
)
Peter Arrenbrecht
peer: introduce real peer classes...
r17192
Pulkit Goyal
py3: conditionalize httplib import...
r29455 httplib = util.httplib
timeless
pycompat: switch to util.urlreq/util.urlerr for py3 compat
r28883 urlerr = util.urlerr
urlreq = util.urlreq
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 def zgenerator(f):
zd = zlib.decompressobj()
try:
for chunk in util.filechunkiter(f):
while chunk:
yield zd.decompress(chunk, 2**18)
chunk = zd.unconsumed_tail
except httplib.HTTPException:
raise IOError(None, _('connection ended unexpectedly'))
yield zd.flush()
class httppeer(wireproto.wirepeer):
def __init__(self, ui, path):
self.path = path
self.caps = None
self.handler = None
self.urlopener = None
Kyle Lippincott
httppeer: allow extensions to replace urllib2.Request...
r25500 self.requestbuilder = None
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 u = util.url(path)
if u.query or u.fragment:
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('unsupported URL component: "%s"') %
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 (u.query or u.fragment))
# urllib cannot handle URLs with embedded user or passwd
self._url, authinfo = u.authinfo()
self.ui = ui
self.ui.debug('using %s\n' % self._url)
self.urlopener = url.opener(ui, authinfo)
timeless
pycompat: switch to util.urlreq/util.urlerr for py3 compat
r28883 self.requestbuilder = urlreq.request
Peter Arrenbrecht
peer: introduce real peer classes...
r17192
def __del__(self):
if self.urlopener:
for h in self.urlopener.handlers:
h.close()
getattr(h, "close_all", lambda : None)()
def url(self):
return self.path
# look up capabilities only when needed
def _fetchcaps(self):
self.caps = set(self._call('capabilities').split())
def _capabilities(self):
if self.caps is None:
try:
self._fetchcaps()
except error.RepoError:
self.caps = set()
self.ui.debug('capabilities: %s\n' %
(' '.join(self.caps or ['none'])))
return self.caps
def lock(self):
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('operation not supported over http'))
Peter Arrenbrecht
peer: introduce real peer classes...
r17192
def _callstream(self, cmd, **args):
if cmd == 'pushkey':
args['data'] = ''
data = args.pop('data', None)
headers = args.pop('headers', {})
self.ui.debug("sending %s command\n" % cmd)
q = [('cmd', cmd)]
headersize = 0
Augie Fackler
http: support sending hgargs via POST body instead of in GET or headers...
r28530 # Important: don't use self.capable() here or else you end up
# with infinite recursion when trying to look up capabilities
# for the first time.
postargsok = self.caps is not None and 'httppostargs' in self.caps
# TODO: support for httppostargs when data is a file-like
# object rather than a basestring
canmungedata = not data or isinstance(data, basestring)
if postargsok and canmungedata:
timeless
pycompat: switch to util.urlreq/util.urlerr for py3 compat
r28883 strargs = urlreq.urlencode(sorted(args.items()))
Augie Fackler
http: support sending hgargs via POST body instead of in GET or headers...
r28530 if strargs:
if not data:
data = strargs
elif isinstance(data, basestring):
data = strargs + data
headers['X-HgArgs-Post'] = len(strargs)
else:
Augie Fackler
httppeer: indent existing argument handling with if True...
r28485 if len(args) > 0:
httpheader = self.capable('httpheader')
if httpheader:
headersize = int(httpheader.split(',', 1)[0])
if headersize > 0:
# The headers can typically carry more data than the URL.
timeless
pycompat: switch to util.urlreq/util.urlerr for py3 compat
r28883 encargs = urlreq.urlencode(sorted(args.items()))
Augie Fackler
httppeer: indent existing argument handling with if True...
r28485 headerfmt = 'X-HgArg-%s'
contentlen = headersize - len(headerfmt % '000' + ': \r\n')
headernum = 0
Augie Fackler
httppeer: compute header names only once...
r28486 varyheaders = []
Augie Fackler
httppeer: indent existing argument handling with if True...
r28485 for i in xrange(0, len(encargs), contentlen):
headernum += 1
header = headerfmt % str(headernum)
headers[header] = encargs[i:i + contentlen]
Augie Fackler
httppeer: compute header names only once...
r28486 varyheaders.append(header)
Augie Fackler
httppeer: indent existing argument handling with if True...
r28485 headers['Vary'] = ','.join(varyheaders)
else:
q += sorted(args.items())
timeless
pycompat: switch to util.urlreq/util.urlerr for py3 compat
r28883 qs = '?%s' % urlreq.urlencode(q)
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 cu = "%s%s" % (self._url, qs)
Augie Fackler
httppeer: move size computation later in _callstream...
r28484 size = 0
if util.safehasattr(data, 'length'):
size = data.length
elif data is not None:
size = len(data)
if size and self.ui.configbool('ui', 'usehttp2', False):
headers['Expect'] = '100-Continue'
headers['X-HgHttp2'] = '1'
if data is not None and 'Content-Type' not in headers:
headers['Content-Type'] = 'application/mercurial-0.1'
Kyle Lippincott
httppeer: allow extensions to replace urllib2.Request...
r25500 req = self.requestbuilder(cu, data, headers)
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 if data is not None:
self.ui.debug("sending %s bytes\n" % size)
req.add_unredirected_header('Content-Length', '%d' % size)
try:
resp = self.urlopener.open(req)
timeless
pycompat: switch to util.urlreq/util.urlerr for py3 compat
r28883 except urlerr.httperror as inst:
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 if inst.code == 401:
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('authorization failed'))
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 raise
Gregory Szorc
global: mass rewrite to use modern exception syntax...
r25660 except httplib.HTTPException as inst:
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 self.ui.debug('http error while sending %s command\n' % cmd)
self.ui.traceback()
raise IOError(None, inst)
except IndexError:
# this only happens with Python 2.3, later versions raise URLError
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('http error, possibly caused by proxy setting'))
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 # record the url we got redirected to
resp_url = resp.geturl()
if resp_url.endswith(qs):
resp_url = resp_url[:-len(qs)]
if self._url.rstrip('/') != resp_url.rstrip('/'):
if not self.ui.quiet:
self.ui.warn(_('real URL is %s\n') % resp_url)
self._url = resp_url
try:
proto = resp.getheader('content-type')
except AttributeError:
proto = resp.headers.get('content-type', '')
safeurl = util.hidepassword(self._url)
if proto.startswith('application/hg-error'):
raise error.OutOfBandError(resp.read())
# accept old "text/plain" and "application/hg-changegroup" for now
if not (proto.startswith('application/mercurial-') or
Matt Mackall
httppeer: improve protocol check...
r18737 (proto.startswith('text/plain')
and not resp.headers.get('content-length')) or
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 proto.startswith('application/hg-changegroup')):
self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
raise error.RepoError(
_("'%s' does not appear to be an hg repository:\n"
"---%%<--- (%s)\n%s\n---%%<---\n")
Matt Mackall
httppeer: avoid large dumps when we don't see an hgweb repo...
r18738 % (safeurl, proto or 'no content-type', resp.read(1024)))
Peter Arrenbrecht
peer: introduce real peer classes...
r17192
if proto.startswith('application/mercurial-'):
try:
version = proto.split('-', 1)[1]
version_info = tuple([int(n) for n in version.split('.')])
except ValueError:
raise error.RepoError(_("'%s' sent a broken Content-Type "
"header (%s)") % (safeurl, proto))
if version_info > (0, 1):
raise error.RepoError(_("'%s' uses newer protocol %s") %
(safeurl, version))
return resp
def _call(self, cmd, **args):
fp = self._callstream(cmd, **args)
try:
return fp.read()
finally:
# if using keepalive, allow connection to be reused
fp.close()
def _callpush(self, cmd, cg, **args):
# have to stream bundle to a temp file because we do not have
# http 1.1 chunked transfer.
types = self.capable('unbundle')
try:
types = types.split(',')
except AttributeError:
# servers older than d1b16a746db6 will send 'unbundle' as a
# boolean capability. They only support headerless/uncompressed
# bundles.
types = [""]
for x in types:
Martin von Zweigbergk
bundle: move writebundle() from changegroup.py to bundle2.py (API)...
r28666 if x in bundle2.bundletypes:
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 type = x
break
Martin von Zweigbergk
bundle: move writebundle() from changegroup.py to bundle2.py (API)...
r28666 tempname = bundle2.writebundle(self.ui, cg, None, type)
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
headers = {'Content-Type': 'application/mercurial-0.1'}
try:
Matt Mackall
httppeer: use try/except/finally
r25085 r = self._call(cmd, data=fp, headers=headers, **args)
vals = r.split('\n', 1)
if len(vals) < 2:
raise error.ResponseError(_("unexpected response:"), r)
return vals
Gregory Szorc
global: mass rewrite to use modern exception syntax...
r25660 except socket.error as err:
Matt Mackall
httppeer: use try/except/finally
r25085 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('push failed: %s') % err.args[1])
raise error.Abort(err.args[1])
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 finally:
fp.close()
os.unlink(tempname)
Pierre-Yves David
httppeer: support for _calltwowaystream...
r21074 def _calltwowaystream(self, cmd, fp, **args):
fh = None
Matt Harbison
httppeer: close the temporary bundle file after two-way streaming it...
r23086 fp_ = None
Pierre-Yves David
httppeer: support for _calltwowaystream...
r21074 filename = None
try:
# dump bundle to disk
fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
fh = os.fdopen(fd, "wb")
d = fp.read(4096)
while d:
fh.write(d)
d = fp.read(4096)
fh.close()
# start http push
Matt Harbison
httppeer: close the temporary bundle file after two-way streaming it...
r23086 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
Pierre-Yves David
httppeer: support for _calltwowaystream...
r21074 headers = {'Content-Type': 'application/mercurial-0.1'}
Matt Harbison
httppeer: close the temporary bundle file after two-way streaming it...
r23086 return self._callstream(cmd, data=fp_, headers=headers, **args)
Pierre-Yves David
httppeer: support for _calltwowaystream...
r21074 finally:
Matt Harbison
httppeer: close the temporary bundle file after two-way streaming it...
r23086 if fp_ is not None:
fp_.close()
Pierre-Yves David
httppeer: support for _calltwowaystream...
r21074 if fh is not None:
fh.close()
os.unlink(filename)
Pierre-Yves David
wireproto: drop the _decompress method in favor a new call type...
r20905 def _callcompressable(self, cmd, **args):
timeless
cleanup: remove superfluous space after space after equals (python)
r27637 stream = self._callstream(cmd, **args)
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 return util.chunkbuffer(zgenerator(stream))
Mads Kiilerich
httppeer: reintroduce _abort that accidentally was removed in 167047ba3cfa...
r21188 def _abort(self, exception):
raise exception
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 class httpspeer(httppeer):
def __init__(self, ui, path):
if not url.has_https:
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('Python support for SSL and HTTPS '
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 'is not installed'))
httppeer.__init__(self, ui, path)
def instance(ui, path, create):
if create:
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('cannot create new http repository'))
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 try:
if path.startswith('https:'):
inst = httpspeer(ui, path)
else:
inst = httppeer(ui, path)
try:
# Try to do useful work when checking compatibility.
# Usually saves a roundtrip since we want the caps anyway.
inst._fetchcaps()
except error.RepoError:
# No luck, try older compatibility check.
inst.between([(nullid, nullid)])
return inst
Gregory Szorc
global: mass rewrite to use modern exception syntax...
r25660 except error.RepoError as httpexception:
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 try:
r = statichttprepo.instance(ui, "static-" + path, create)
FUJIWARA Katsunori
httppeer: make a message translatable...
r29241 ui.note(_('(falling back to static-http)\n'))
Peter Arrenbrecht
peer: introduce real peer classes...
r17192 return r
except error.RepoError:
raise httpexception # use the original http RepoError instead