121
|
1 # httprepo.py - HTTP repository proxy classes for mercurial
|
|
2 #
|
|
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
|
|
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
|
|
5 #
|
|
6 # This software may be used and distributed according to the terms of the
|
|
7 # GNU General Public License version 2, incorporated herein by reference.
|
|
8
|
|
9 from node import bin, hex, nullid
|
|
10 from i18n import _
|
|
11 import repo, changegroup, statichttprepo, error, url, util
|
|
12 import os, urllib, urllib2, urlparse, zlib, httplib
|
|
13 import errno, socket
|
|
14
|
|
15 def zgenerator(f):
|
|
16 zd = zlib.decompressobj()
|
|
17 try:
|
|
18 for chunk in util.filechunkiter(f):
|
|
19 yield zd.decompress(chunk)
|
|
20 except httplib.HTTPException:
|
|
21 raise IOError(None, _('connection ended unexpectedly'))
|
|
22 yield zd.flush()
|
|
23
|
|
24 class httprepository(repo.repository):
|
|
25 def __init__(self, ui, path):
|
|
26 self.path = path
|
|
27 self.caps = None
|
|
28 self.handler = None
|
|
29 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
|
|
30 if query or frag:
|
|
31 raise util.Abort(_('unsupported URL component: "%s"') %
|
|
32 (query or frag))
|
|
33
|
|
34 # urllib cannot handle URLs with embedded user or passwd
|
|
35 self._url, authinfo = url.getauthinfo(path)
|
|
36
|
|
37 self.ui = ui
|
|
38 self.ui.debug(_('using %s\n') % self._url)
|
|
39
|
|
40 self.urlopener = url.opener(ui, authinfo)
|
|
41
|
|
42 def __del__(self):
|
|
43 for h in self.urlopener.handlers:
|
|
44 h.close()
|
|
45 if hasattr(h, "close_all"):
|
|
46 h.close_all()
|
|
47
|
|
48 def url(self):
|
|
49 return self.path
|
|
50
|
|
51 # look up capabilities only when needed
|
|
52
|
|
53 def get_caps(self):
|
|
54 if self.caps is None:
|
|
55 try:
|
|
56 self.caps = set(self.do_read('capabilities').split())
|
|
57 except error.RepoError:
|
|
58 self.caps = set()
|
|
59 self.ui.debug(_('capabilities: %s\n') %
|
|
60 (' '.join(self.caps or ['none'])))
|
|
61 return self.caps
|
|
62
|
|
63 capabilities = property(get_caps)
|
|
64
|
|
65 def lock(self):
|
|
66 raise util.Abort(_('operation not supported over http'))
|
|
67
|
|
68 def do_cmd(self, cmd, **args):
|
|
69 data = args.pop('data', None)
|
|
70 headers = args.pop('headers', {})
|
|
71 self.ui.debug(_("sending %s command\n") % cmd)
|
|
72 q = {"cmd": cmd}
|
|
73 q.update(args)
|
|
74 qs = '?%s' % urllib.urlencode(q)
|
|
75 cu = "%s%s" % (self._url, qs)
|
|
76 try:
|
|
77 if data:
|
|
78 self.ui.debug(_("sending %s bytes\n") % len(data))
|
|
79 resp = self.urlopener.open(urllib2.Request(cu, data, headers))
|
|
80 except urllib2.HTTPError, inst:
|
|
81 if inst.code == 401:
|
|
82 raise util.Abort(_('authorization failed'))
|
|
83 raise
|
|
84 except httplib.HTTPException, inst:
|
|
85 self.ui.debug(_('http error while sending %s command\n') % cmd)
|
|
86 self.ui.traceback()
|
|
87 raise IOError(None, inst)
|
|
88 except IndexError:
|
|
89 # this only happens with Python 2.3, later versions raise URLError
|
|
90 raise util.Abort(_('http error, possibly caused by proxy setting'))
|
|
91 # record the url we got redirected to
|
|
92 resp_url = resp.geturl()
|
|
93 if resp_url.endswith(qs):
|
|
94 resp_url = resp_url[:-len(qs)]
|
|
95 if self._url != resp_url:
|
|
96 self.ui.status(_('real URL is %s\n') % resp_url)
|
|
97 self._url = resp_url
|
|
98 try:
|
|
99 proto = resp.getheader('content-type')
|
|
100 except AttributeError:
|
|
101 proto = resp.headers['content-type']
|
|
102
|
|
103 safeurl = url.hidepassword(self._url)
|
|
104 # accept old "text/plain" and "application/hg-changegroup" for now
|
|
105 if not (proto.startswith('application/mercurial-') or
|
|
106 proto.startswith('text/plain') or
|
|
107 proto.startswith('application/hg-changegroup')):
|
|
108 self.ui.debug(_("requested URL: '%s'\n") % url.hidepassword(cu))
|
|
109 raise error.RepoError(_("'%s' does not appear to be an hg repository")
|
|
110 % safeurl)
|
|
111
|
|
112 if proto.startswith('application/mercurial-'):
|
|
113 try:
|
|
114 version = proto.split('-', 1)[1]
|
|
115 version_info = tuple([int(n) for n in version.split('.')])
|
|
116 except ValueError:
|
|
117 raise error.RepoError(_("'%s' sent a broken Content-Type "
|
|
118 "header (%s)") % (safeurl, proto))
|
|
119 if version_info > (0, 1):
|
|
120 raise error.RepoError(_("'%s' uses newer protocol %s") %
|
|
121 (safeurl, version))
|
|
122
|
|
123 return resp
|
|
124
|
|
125 def do_read(self, cmd, **args):
|
|
126 fp = self.do_cmd(cmd, **args)
|
|
127 try:
|
|
128 return fp.read()
|
|
129 finally:
|
|
130 # if using keepalive, allow connection to be reused
|
|
131 fp.close()
|
|
132
|
|
133 def lookup(self, key):
|
|
134 self.requirecap('lookup', _('look up remote revision'))
|
|
135 d = self.do_cmd("lookup", key = key).read()
|
|
136 success, data = d[:-1].split(' ', 1)
|
|
137 if int(success):
|
|
138 return bin(data)
|
|
139 raise error.RepoError(data)
|
|
140
|
|
141 def heads(self):
|
|
142 d = self.do_read("heads")
|
|
143 try:
|
|
144 return map(bin, d[:-1].split(" "))
|
|
145 except:
|
|
146 raise error.ResponseError(_("unexpected response:"), d)
|
|
147
|
|
148 def branchmap(self):
|
|
149 d = self.do_read("branchmap")
|
|
150 try:
|
|
151 branchmap = {}
|
|
152 for branchpart in d.splitlines():
|
|
153 branchheads = branchpart.split(' ')
|
|
154 branchname = urllib.unquote(branchheads[0])
|
|
155 branchheads = [bin(x) for x in branchheads[1:]]
|
|
156 branchmap[branchname] = branchheads
|
|
157 return branchmap
|
|
158 except:
|
|
159 raise error.ResponseError(_("unexpected response:"), d)
|
|
160
|
|
161 def branches(self, nodes):
|
|
162 n = " ".join(map(hex, nodes))
|
|
163 d = self.do_read("branches", nodes=n)
|
|
164 try:
|
|
165 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
|
|
166 return br
|
|
167 except:
|
|
168 raise error.ResponseError(_("unexpected response:"), d)
|
|
169
|
|
170 def between(self, pairs):
|
|
171 batch = 8 # avoid giant requests
|
|
172 r = []
|
|
173 for i in xrange(0, len(pairs), batch):
|
|
174 n = " ".join(["-".join(map(hex, p)) for p in pairs[i:i + batch]])
|
|
175 d = self.do_read("between", pairs=n)
|
|
176 try:
|
|
177 r += [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
|
|
178 except:
|
|
179 raise error.ResponseError(_("unexpected response:"), d)
|
|
180 return r
|
|
181
|
|
182 def changegroup(self, nodes, kind):
|
|
183 n = " ".join(map(hex, nodes))
|
|
184 f = self.do_cmd("changegroup", roots=n)
|
|
185 return util.chunkbuffer(zgenerator(f))
|
|
186
|
|
187 def changegroupsubset(self, bases, heads, source):
|
|
188 self.requirecap('changegroupsubset', _('look up remote changes'))
|
|
189 baselst = " ".join([hex(n) for n in bases])
|
|
190 headlst = " ".join([hex(n) for n in heads])
|
|
191 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
|
|
192 return util.chunkbuffer(zgenerator(f))
|
|
193
|
|
194 def unbundle(self, cg, heads, source):
|
|
195 # have to stream bundle to a temp file because we do not have
|
|
196 # http 1.1 chunked transfer.
|
|
197
|
|
198 type = ""
|
|
199 types = self.capable('unbundle')
|
|
200 # servers older than d1b16a746db6 will send 'unbundle' as a
|
|
201 # boolean capability
|
|
202 try:
|
|
203 types = types.split(',')
|
|
204 except AttributeError:
|
|
205 types = [""]
|
|
206 if types:
|
|
207 for x in types:
|
|
208 if x in changegroup.bundletypes:
|
|
209 type = x
|
|
210 break
|
|
211
|
|
212 tempname = changegroup.writebundle(cg, None, type)
|
|
213 fp = url.httpsendfile(tempname, "rb")
|
|
214 try:
|
|
215 try:
|
|
216 resp = self.do_read(
|
|
217 'unbundle', data=fp,
|
|
218 headers={'Content-Type': 'application/octet-stream'},
|
|
219 heads=' '.join(map(hex, heads)))
|
|
220 resp_code, output = resp.split('\n', 1)
|
|
221 try:
|
|
222 ret = int(resp_code)
|
|
223 except ValueError, err:
|
|
224 raise error.ResponseError(
|
|
225 _('push failed (unexpected response):'), resp)
|
|
226 self.ui.write(output)
|
|
227 return ret
|
|
228 except socket.error, err:
|
|
229 if err[0] in (errno.ECONNRESET, errno.EPIPE):
|
|
230 raise util.Abort(_('push failed: %s') % err[1])
|
|
231 raise util.Abort(err[1])
|
|
232 finally:
|
|
233 fp.close()
|
|
234 os.unlink(tempname)
|
|
235
|
|
236 def stream_out(self):
|
|
237 return self.do_cmd('stream_out')
|
|
238
|
|
239 class httpsrepository(httprepository):
|
|
240 def __init__(self, ui, path):
|
|
241 if not url.has_https:
|
|
242 raise util.Abort(_('Python support for SSL and HTTPS '
|
|
243 'is not installed'))
|
|
244 httprepository.__init__(self, ui, path)
|
|
245
|
|
246 def instance(ui, path, create):
|
|
247 if create:
|
|
248 raise util.Abort(_('cannot create new http repository'))
|
|
249 try:
|
|
250 if path.startswith('https:'):
|
|
251 inst = httpsrepository(ui, path)
|
|
252 else:
|
|
253 inst = httprepository(ui, path)
|
|
254 inst.between([(nullid, nullid)])
|
|
255 return inst
|
|
256 except error.RepoError:
|
|
257 ui.note('(falling back to static-http)\n')
|
|
258 return statichttprepo.instance(ui, "static-" + path, create)
|