httprepo.py
235 lines
| 8.4 KiB
| text/x-python
|
PythonLexer
/ mercurial / httprepo.py
|
r1089 | # httprepo.py - HTTP repository proxy classes for mercurial | ||
# | ||||
|
r2859 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
|
r1089 | # | ||
# This software may be used and distributed according to the terms | ||||
# of the GNU General Public License, incorporated herein by reference. | ||||
|
r7211 | from node import bin, hex, nullid | ||
|
r3891 | from i18n import _ | ||
|
r5176 | import repo, os, urllib, urllib2, urlparse, zlib, util, httplib | ||
|
r7280 | import errno, socket, changegroup, statichttprepo | ||
|
r7270 | import url | ||
|
r4678 | |||
|
r3661 | def zgenerator(f): | ||
zd = zlib.decompressobj() | ||||
try: | ||||
for chunk in util.filechunkiter(f): | ||||
yield zd.decompress(chunk) | ||||
|
r7280 | except httplib.HTTPException: | ||
|
r3661 | raise IOError(None, _('connection ended unexpectedly')) | ||
yield zd.flush() | ||||
|
r6313 | class httprepository(repo.repository): | ||
|
r1089 | def __init__(self, ui, path): | ||
|
r2673 | self.path = path | ||
|
r2442 | self.caps = None | ||
|
r4132 | self.handler = None | ||
|
r2337 | scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) | ||
if query or frag: | ||||
raise util.Abort(_('unsupported URL component: "%s"') % | ||||
(query or frag)) | ||||
# urllib cannot handle URLs with embedded user or passwd | ||||
|
r7270 | self._url, authinfo = url.getauthinfo(path) | ||
|
r1089 | self.ui = ui | ||
|
r5066 | self.ui.debug(_('using %s\n') % self._url) | ||
|
r2337 | |||
|
r7270 | self.urlopener = url.opener(ui, authinfo) | ||
|
r4516 | |||
|
r2673 | def url(self): | ||
return self.path | ||||
|
r2442 | # look up capabilities only when needed | ||
def get_caps(self): | ||||
if self.caps is None: | ||||
try: | ||||
|
r5258 | self.caps = util.set(self.do_read('capabilities').split()) | ||
|
r5176 | except repo.RepoError: | ||
|
r5258 | self.caps = util.set() | ||
|
r2465 | self.ui.debug(_('capabilities: %s\n') % | ||
(' '.join(self.caps or ['none']))) | ||||
|
r2442 | return self.caps | ||
capabilities = property(get_caps) | ||||
|
r1870 | def lock(self): | ||
raise util.Abort(_('operation not supported over http')) | ||||
|
r1089 | def do_cmd(self, cmd, **args): | ||
|
r2465 | data = args.pop('data', None) | ||
headers = args.pop('headers', {}) | ||||
|
r1402 | self.ui.debug(_("sending %s command\n") % cmd) | ||
|
r1089 | q = {"cmd": cmd} | ||
q.update(args) | ||||
|
r3562 | qs = '?%s' % urllib.urlencode(q) | ||
cu = "%s%s" % (self._url, qs) | ||||
|
r2294 | try: | ||
|
r3567 | if data: | ||
|
r5333 | self.ui.debug(_("sending %s bytes\n") % len(data)) | ||
|
r7270 | resp = self.urlopener.open(urllib2.Request(cu, data, headers)) | ||
|
r2467 | except urllib2.HTTPError, inst: | ||
if inst.code == 401: | ||||
raise util.Abort(_('authorization failed')) | ||||
raise | ||||
|
r2294 | except httplib.HTTPException, inst: | ||
|
r2336 | self.ui.debug(_('http error while sending %s command\n') % cmd) | ||
self.ui.print_exc() | ||||
raise IOError(None, inst) | ||||
|
r3399 | except IndexError: | ||
# this only happens with Python 2.3, later versions raise URLError | ||||
raise util.Abort(_('http error, possibly caused by proxy setting')) | ||||
|
r3562 | # record the url we got redirected to | ||
|
r3570 | resp_url = resp.geturl() | ||
if resp_url.endswith(qs): | ||||
resp_url = resp_url[:-len(qs)] | ||||
if self._url != resp_url: | ||||
self.ui.status(_('real URL is %s\n') % resp_url) | ||||
self._url = resp_url | ||||
|
r2435 | try: | ||
proto = resp.getheader('content-type') | ||||
except AttributeError: | ||||
proto = resp.headers['content-type'] | ||||
|
r1089 | |||
# accept old "text/plain" and "application/hg-changegroup" for now | ||||
|
r4633 | if not (proto.startswith('application/mercurial-') or | ||
proto.startswith('text/plain') or | ||||
proto.startswith('application/hg-changegroup')): | ||||
|
r4739 | self.ui.debug(_("Requested URL: '%s'\n") % cu) | ||
|
r5176 | raise repo.RepoError(_("'%s' does not appear to be an hg repository") | ||
|
r4633 | % self._url) | ||
|
r1089 | |||
|
r4012 | if proto.startswith('application/mercurial-'): | ||
try: | ||||
|
r4356 | version = proto.split('-', 1)[1] | ||
version_info = tuple([int(n) for n in version.split('.')]) | ||||
|
r4012 | except ValueError: | ||
|
r5930 | raise repo.RepoError(_("'%s' sent a broken Content-Type " | ||
|
r4012 | "header (%s)") % (self._url, proto)) | ||
|
r4356 | if version_info > (0, 1): | ||
|
r5176 | raise repo.RepoError(_("'%s' uses newer protocol %s") % | ||
|
r2673 | (self._url, version)) | ||
|
r1089 | |||
return resp | ||||
|
r2435 | def do_read(self, cmd, **args): | ||
fp = self.do_cmd(cmd, **args) | ||||
try: | ||||
return fp.read() | ||||
finally: | ||||
# if using keepalive, allow connection to be reused | ||||
fp.close() | ||||
|
r3444 | def lookup(self, key): | ||
|
r5259 | self.requirecap('lookup', _('look up remote revision')) | ||
|
r3445 | d = self.do_cmd("lookup", key = key).read() | ||
success, data = d[:-1].split(' ', 1) | ||||
if int(success): | ||||
return bin(data) | ||||
|
r5176 | raise repo.RepoError(data) | ||
|
r3444 | |||
|
r1089 | def heads(self): | ||
|
r2435 | d = self.do_read("heads") | ||
|
r1089 | try: | ||
return map(bin, d[:-1].split(" ")) | ||||
except: | ||||
|
r3565 | raise util.UnexpectedOutput(_("unexpected response:"), d) | ||
|
r1089 | |||
def branches(self, nodes): | ||||
n = " ".join(map(hex, nodes)) | ||||
|
r2435 | d = self.do_read("branches", nodes=n) | ||
|
r1089 | try: | ||
br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ] | ||||
return br | ||||
except: | ||||
|
r3565 | raise util.UnexpectedOutput(_("unexpected response:"), d) | ||
|
r1089 | |||
def between(self, pairs): | ||||
|
r7207 | n = " ".join(["-".join(map(hex, p)) for p in pairs]) | ||
|
r2435 | d = self.do_read("between", pairs=n) | ||
|
r1089 | try: | ||
p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ] | ||||
return p | ||||
except: | ||||
|
r3565 | raise util.UnexpectedOutput(_("unexpected response:"), d) | ||
|
r1089 | |||
|
r1736 | def changegroup(self, nodes, kind): | ||
|
r1089 | n = " ".join(map(hex, nodes)) | ||
f = self.do_cmd("changegroup", roots=n) | ||||
|
r3661 | return util.chunkbuffer(zgenerator(f)) | ||
|
r3444 | |||
def changegroupsubset(self, bases, heads, source): | ||||
|
r5259 | self.requirecap('changegroupsubset', _('look up remote changes')) | ||
|
r3444 | baselst = " ".join([hex(n) for n in bases]) | ||
headlst = " ".join([hex(n) for n in heads]) | ||||
f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst) | ||||
|
r3661 | return util.chunkbuffer(zgenerator(f)) | ||
|
r1089 | |||
|
r2439 | def unbundle(self, cg, heads, source): | ||
|
r2465 | # have to stream bundle to a temp file because we do not have | ||
# http 1.1 chunked transfer. | ||||
|
r3662 | type = "" | ||
types = self.capable('unbundle') | ||||
|
r3703 | # servers older than d1b16a746db6 will send 'unbundle' as a | ||
# boolean capability | ||||
try: | ||||
types = types.split(',') | ||||
except AttributeError: | ||||
types = [""] | ||||
|
r3662 | if types: | ||
|
r3703 | for x in types: | ||
|
r3662 | if x in changegroup.bundletypes: | ||
type = x | ||||
break | ||||
|
r3613 | |||
|
r3662 | tempname = changegroup.writebundle(cg, None, type) | ||
|
r7270 | fp = url.httpsendfile(tempname, "rb") | ||
|
r2465 | try: | ||
try: | ||||
|
r7010 | resp = self.do_read( | ||
'unbundle', data=fp, | ||||
headers={'Content-Type': 'application/octet-stream'}, | ||||
heads=' '.join(map(hex, heads))) | ||||
resp_code, output = resp.split('\n', 1) | ||||
|
r2467 | try: | ||
|
r7010 | ret = int(resp_code) | ||
except ValueError, err: | ||||
raise util.UnexpectedOutput( | ||||
_('push failed (unexpected response):'), resp) | ||||
self.ui.write(output) | ||||
return ret | ||||
|
r2467 | except socket.error, err: | ||
if err[0] in (errno.ECONNRESET, errno.EPIPE): | ||||
|
r3072 | raise util.Abort(_('push failed: %s') % err[1]) | ||
|
r2467 | raise util.Abort(err[1]) | ||
|
r2465 | finally: | ||
fp.close() | ||||
os.unlink(tempname) | ||||
|
r2439 | |||
|
r2612 | def stream_out(self): | ||
return self.do_cmd('stream_out') | ||||
|
r1089 | class httpsrepository(httprepository): | ||
|
r2569 | def __init__(self, ui, path): | ||
|
r7279 | if not url.has_https: | ||
|
r2569 | raise util.Abort(_('Python support for SSL and HTTPS ' | ||
'is not installed')) | ||||
httprepository.__init__(self, ui, path) | ||||
|
r2740 | |||
def instance(ui, path, create): | ||||
if create: | ||||
raise util.Abort(_('cannot create new http repository')) | ||||
|
r7211 | try: | ||
if path.startswith('https:'): | ||||
inst = httpsrepository(ui, path) | ||||
else: | ||||
inst = httprepository(ui, path) | ||||
inst.between([(nullid, nullid)]) | ||||
return inst | ||||
except repo.RepoError: | ||||
ui.note('(falling back to static-http)\n') | ||||
return statichttprepo.instance(ui, "static-" + path, create) | ||||