##// END OF EJS Templates
send conservatively capitalized HTTP headers
Dirkjan Ochtman -
r5930:c301f15c default
parent child Browse files
Show More
@@ -1,469 +1,469
1 # This library is free software; you can redistribute it and/or
1 # This library is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU Lesser General Public
2 # modify it under the terms of the GNU Lesser General Public
3 # License as published by the Free Software Foundation; either
3 # License as published by the Free Software Foundation; either
4 # version 2.1 of the License, or (at your option) any later version.
4 # version 2.1 of the License, or (at your option) any later version.
5 #
5 #
6 # This library is distributed in the hope that it will be useful,
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
9 # Lesser General Public License for more details.
10 #
10 #
11 # You should have received a copy of the GNU Lesser General Public
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, write to the
12 # License along with this library; if not, write to the
13 # Free Software Foundation, Inc.,
13 # Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330,
14 # 59 Temple Place, Suite 330,
15 # Boston, MA 02111-1307 USA
15 # Boston, MA 02111-1307 USA
16
16
17 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
17 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
18 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
18 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
19
19
20 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
20 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
21
21
22 import os
22 import os
23 import stat
23 import stat
24 import urllib
24 import urllib
25 import urllib2
25 import urllib2
26 import rfc822
26 import rfc822
27
27
28 try:
28 try:
29 from cStringIO import StringIO
29 from cStringIO import StringIO
30 except ImportError, msg:
30 except ImportError, msg:
31 from StringIO import StringIO
31 from StringIO import StringIO
32
32
33 class RangeError(IOError):
33 class RangeError(IOError):
34 """Error raised when an unsatisfiable range is requested."""
34 """Error raised when an unsatisfiable range is requested."""
35 pass
35 pass
36
36
37 class HTTPRangeHandler(urllib2.BaseHandler):
37 class HTTPRangeHandler(urllib2.BaseHandler):
38 """Handler that enables HTTP Range headers.
38 """Handler that enables HTTP Range headers.
39
39
40 This was extremely simple. The Range header is a HTTP feature to
40 This was extremely simple. The Range header is a HTTP feature to
41 begin with so all this class does is tell urllib2 that the
41 begin with so all this class does is tell urllib2 that the
42 "206 Partial Content" reponse from the HTTP server is what we
42 "206 Partial Content" reponse from the HTTP server is what we
43 expected.
43 expected.
44
44
45 Example:
45 Example:
46 import urllib2
46 import urllib2
47 import byterange
47 import byterange
48
48
49 range_handler = range.HTTPRangeHandler()
49 range_handler = range.HTTPRangeHandler()
50 opener = urllib2.build_opener(range_handler)
50 opener = urllib2.build_opener(range_handler)
51
51
52 # install it
52 # install it
53 urllib2.install_opener(opener)
53 urllib2.install_opener(opener)
54
54
55 # create Request and set Range header
55 # create Request and set Range header
56 req = urllib2.Request('http://www.python.org/')
56 req = urllib2.Request('http://www.python.org/')
57 req.header['Range'] = 'bytes=30-50'
57 req.header['Range'] = 'bytes=30-50'
58 f = urllib2.urlopen(req)
58 f = urllib2.urlopen(req)
59 """
59 """
60
60
61 def http_error_206(self, req, fp, code, msg, hdrs):
61 def http_error_206(self, req, fp, code, msg, hdrs):
62 # 206 Partial Content Response
62 # 206 Partial Content Response
63 r = urllib.addinfourl(fp, hdrs, req.get_full_url())
63 r = urllib.addinfourl(fp, hdrs, req.get_full_url())
64 r.code = code
64 r.code = code
65 r.msg = msg
65 r.msg = msg
66 return r
66 return r
67
67
68 def http_error_416(self, req, fp, code, msg, hdrs):
68 def http_error_416(self, req, fp, code, msg, hdrs):
69 # HTTP's Range Not Satisfiable error
69 # HTTP's Range Not Satisfiable error
70 raise RangeError('Requested Range Not Satisfiable')
70 raise RangeError('Requested Range Not Satisfiable')
71
71
72 class RangeableFileObject:
72 class RangeableFileObject:
73 """File object wrapper to enable raw range handling.
73 """File object wrapper to enable raw range handling.
74 This was implemented primarilary for handling range
74 This was implemented primarilary for handling range
75 specifications for file:// urls. This object effectively makes
75 specifications for file:// urls. This object effectively makes
76 a file object look like it consists only of a range of bytes in
76 a file object look like it consists only of a range of bytes in
77 the stream.
77 the stream.
78
78
79 Examples:
79 Examples:
80 # expose 10 bytes, starting at byte position 20, from
80 # expose 10 bytes, starting at byte position 20, from
81 # /etc/aliases.
81 # /etc/aliases.
82 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
82 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
83 # seek seeks within the range (to position 23 in this case)
83 # seek seeks within the range (to position 23 in this case)
84 >>> fo.seek(3)
84 >>> fo.seek(3)
85 # tell tells where your at _within the range_ (position 3 in
85 # tell tells where your at _within the range_ (position 3 in
86 # this case)
86 # this case)
87 >>> fo.tell()
87 >>> fo.tell()
88 # read EOFs if an attempt is made to read past the last
88 # read EOFs if an attempt is made to read past the last
89 # byte in the range. the following will return only 7 bytes.
89 # byte in the range. the following will return only 7 bytes.
90 >>> fo.read(30)
90 >>> fo.read(30)
91 """
91 """
92
92
93 def __init__(self, fo, rangetup):
93 def __init__(self, fo, rangetup):
94 """Create a RangeableFileObject.
94 """Create a RangeableFileObject.
95 fo -- a file like object. only the read() method need be
95 fo -- a file like object. only the read() method need be
96 supported but supporting an optimized seek() is
96 supported but supporting an optimized seek() is
97 preferable.
97 preferable.
98 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
98 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
99 to work over.
99 to work over.
100 The file object provided is assumed to be at byte offset 0.
100 The file object provided is assumed to be at byte offset 0.
101 """
101 """
102 self.fo = fo
102 self.fo = fo
103 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
103 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
104 self.realpos = 0
104 self.realpos = 0
105 self._do_seek(self.firstbyte)
105 self._do_seek(self.firstbyte)
106
106
107 def __getattr__(self, name):
107 def __getattr__(self, name):
108 """This effectively allows us to wrap at the instance level.
108 """This effectively allows us to wrap at the instance level.
109 Any attribute not found in _this_ object will be searched for
109 Any attribute not found in _this_ object will be searched for
110 in self.fo. This includes methods."""
110 in self.fo. This includes methods."""
111 if hasattr(self.fo, name):
111 if hasattr(self.fo, name):
112 return getattr(self.fo, name)
112 return getattr(self.fo, name)
113 raise AttributeError, name
113 raise AttributeError, name
114
114
115 def tell(self):
115 def tell(self):
116 """Return the position within the range.
116 """Return the position within the range.
117 This is different from fo.seek in that position 0 is the
117 This is different from fo.seek in that position 0 is the
118 first byte position of the range tuple. For example, if
118 first byte position of the range tuple. For example, if
119 this object was created with a range tuple of (500,899),
119 this object was created with a range tuple of (500,899),
120 tell() will return 0 when at byte position 500 of the file.
120 tell() will return 0 when at byte position 500 of the file.
121 """
121 """
122 return (self.realpos - self.firstbyte)
122 return (self.realpos - self.firstbyte)
123
123
124 def seek(self, offset, whence=0):
124 def seek(self, offset, whence=0):
125 """Seek within the byte range.
125 """Seek within the byte range.
126 Positioning is identical to that described under tell().
126 Positioning is identical to that described under tell().
127 """
127 """
128 assert whence in (0, 1, 2)
128 assert whence in (0, 1, 2)
129 if whence == 0: # absolute seek
129 if whence == 0: # absolute seek
130 realoffset = self.firstbyte + offset
130 realoffset = self.firstbyte + offset
131 elif whence == 1: # relative seek
131 elif whence == 1: # relative seek
132 realoffset = self.realpos + offset
132 realoffset = self.realpos + offset
133 elif whence == 2: # absolute from end of file
133 elif whence == 2: # absolute from end of file
134 # XXX: are we raising the right Error here?
134 # XXX: are we raising the right Error here?
135 raise IOError('seek from end of file not supported.')
135 raise IOError('seek from end of file not supported.')
136
136
137 # do not allow seek past lastbyte in range
137 # do not allow seek past lastbyte in range
138 if self.lastbyte and (realoffset >= self.lastbyte):
138 if self.lastbyte and (realoffset >= self.lastbyte):
139 realoffset = self.lastbyte
139 realoffset = self.lastbyte
140
140
141 self._do_seek(realoffset - self.realpos)
141 self._do_seek(realoffset - self.realpos)
142
142
143 def read(self, size=-1):
143 def read(self, size=-1):
144 """Read within the range.
144 """Read within the range.
145 This method will limit the size read based on the range.
145 This method will limit the size read based on the range.
146 """
146 """
147 size = self._calc_read_size(size)
147 size = self._calc_read_size(size)
148 rslt = self.fo.read(size)
148 rslt = self.fo.read(size)
149 self.realpos += len(rslt)
149 self.realpos += len(rslt)
150 return rslt
150 return rslt
151
151
152 def readline(self, size=-1):
152 def readline(self, size=-1):
153 """Read lines within the range.
153 """Read lines within the range.
154 This method will limit the size read based on the range.
154 This method will limit the size read based on the range.
155 """
155 """
156 size = self._calc_read_size(size)
156 size = self._calc_read_size(size)
157 rslt = self.fo.readline(size)
157 rslt = self.fo.readline(size)
158 self.realpos += len(rslt)
158 self.realpos += len(rslt)
159 return rslt
159 return rslt
160
160
161 def _calc_read_size(self, size):
161 def _calc_read_size(self, size):
162 """Handles calculating the amount of data to read based on
162 """Handles calculating the amount of data to read based on
163 the range.
163 the range.
164 """
164 """
165 if self.lastbyte:
165 if self.lastbyte:
166 if size > -1:
166 if size > -1:
167 if ((self.realpos + size) >= self.lastbyte):
167 if ((self.realpos + size) >= self.lastbyte):
168 size = (self.lastbyte - self.realpos)
168 size = (self.lastbyte - self.realpos)
169 else:
169 else:
170 size = (self.lastbyte - self.realpos)
170 size = (self.lastbyte - self.realpos)
171 return size
171 return size
172
172
173 def _do_seek(self, offset):
173 def _do_seek(self, offset):
174 """Seek based on whether wrapped object supports seek().
174 """Seek based on whether wrapped object supports seek().
175 offset is relative to the current position (self.realpos).
175 offset is relative to the current position (self.realpos).
176 """
176 """
177 assert offset >= 0
177 assert offset >= 0
178 if not hasattr(self.fo, 'seek'):
178 if not hasattr(self.fo, 'seek'):
179 self._poor_mans_seek(offset)
179 self._poor_mans_seek(offset)
180 else:
180 else:
181 self.fo.seek(self.realpos + offset)
181 self.fo.seek(self.realpos + offset)
182 self.realpos += offset
182 self.realpos += offset
183
183
184 def _poor_mans_seek(self, offset):
184 def _poor_mans_seek(self, offset):
185 """Seek by calling the wrapped file objects read() method.
185 """Seek by calling the wrapped file objects read() method.
186 This is used for file like objects that do not have native
186 This is used for file like objects that do not have native
187 seek support. The wrapped objects read() method is called
187 seek support. The wrapped objects read() method is called
188 to manually seek to the desired position.
188 to manually seek to the desired position.
189 offset -- read this number of bytes from the wrapped
189 offset -- read this number of bytes from the wrapped
190 file object.
190 file object.
191 raise RangeError if we encounter EOF before reaching the
191 raise RangeError if we encounter EOF before reaching the
192 specified offset.
192 specified offset.
193 """
193 """
194 pos = 0
194 pos = 0
195 bufsize = 1024
195 bufsize = 1024
196 while pos < offset:
196 while pos < offset:
197 if (pos + bufsize) > offset:
197 if (pos + bufsize) > offset:
198 bufsize = offset - pos
198 bufsize = offset - pos
199 buf = self.fo.read(bufsize)
199 buf = self.fo.read(bufsize)
200 if len(buf) != bufsize:
200 if len(buf) != bufsize:
201 raise RangeError('Requested Range Not Satisfiable')
201 raise RangeError('Requested Range Not Satisfiable')
202 pos += bufsize
202 pos += bufsize
203
203
204 class FileRangeHandler(urllib2.FileHandler):
204 class FileRangeHandler(urllib2.FileHandler):
205 """FileHandler subclass that adds Range support.
205 """FileHandler subclass that adds Range support.
206 This class handles Range headers exactly like an HTTP
206 This class handles Range headers exactly like an HTTP
207 server would.
207 server would.
208 """
208 """
209 def open_local_file(self, req):
209 def open_local_file(self, req):
210 import mimetypes
210 import mimetypes
211 import mimetools
211 import mimetools
212 host = req.get_host()
212 host = req.get_host()
213 file = req.get_selector()
213 file = req.get_selector()
214 localfile = urllib.url2pathname(file)
214 localfile = urllib.url2pathname(file)
215 stats = os.stat(localfile)
215 stats = os.stat(localfile)
216 size = stats[stat.ST_SIZE]
216 size = stats[stat.ST_SIZE]
217 modified = rfc822.formatdate(stats[stat.ST_MTIME])
217 modified = rfc822.formatdate(stats[stat.ST_MTIME])
218 mtype = mimetypes.guess_type(file)[0]
218 mtype = mimetypes.guess_type(file)[0]
219 if host:
219 if host:
220 host, port = urllib.splitport(host)
220 host, port = urllib.splitport(host)
221 if port or socket.gethostbyname(host) not in self.get_names():
221 if port or socket.gethostbyname(host) not in self.get_names():
222 raise urllib2.URLError('file not on local host')
222 raise urllib2.URLError('file not on local host')
223 fo = open(localfile,'rb')
223 fo = open(localfile,'rb')
224 brange = req.headers.get('Range', None)
224 brange = req.headers.get('Range', None)
225 brange = range_header_to_tuple(brange)
225 brange = range_header_to_tuple(brange)
226 assert brange != ()
226 assert brange != ()
227 if brange:
227 if brange:
228 (fb, lb) = brange
228 (fb, lb) = brange
229 if lb == '':
229 if lb == '':
230 lb = size
230 lb = size
231 if fb < 0 or fb > size or lb > size:
231 if fb < 0 or fb > size or lb > size:
232 raise RangeError('Requested Range Not Satisfiable')
232 raise RangeError('Requested Range Not Satisfiable')
233 size = (lb - fb)
233 size = (lb - fb)
234 fo = RangeableFileObject(fo, (fb, lb))
234 fo = RangeableFileObject(fo, (fb, lb))
235 headers = mimetools.Message(StringIO(
235 headers = mimetools.Message(StringIO(
236 'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
236 'Content-Type: %s\nContent-Length: %d\nLast-Modified: %s\n' %
237 (mtype or 'text/plain', size, modified)))
237 (mtype or 'text/plain', size, modified)))
238 return urllib.addinfourl(fo, headers, 'file:'+file)
238 return urllib.addinfourl(fo, headers, 'file:'+file)
239
239
240
240
241 # FTP Range Support
241 # FTP Range Support
242 # Unfortunately, a large amount of base FTP code had to be copied
242 # Unfortunately, a large amount of base FTP code had to be copied
243 # from urllib and urllib2 in order to insert the FTP REST command.
243 # from urllib and urllib2 in order to insert the FTP REST command.
244 # Code modifications for range support have been commented as
244 # Code modifications for range support have been commented as
245 # follows:
245 # follows:
246 # -- range support modifications start/end here
246 # -- range support modifications start/end here
247
247
248 from urllib import splitport, splituser, splitpasswd, splitattr, \
248 from urllib import splitport, splituser, splitpasswd, splitattr, \
249 unquote, addclosehook, addinfourl
249 unquote, addclosehook, addinfourl
250 import ftplib
250 import ftplib
251 import socket
251 import socket
252 import sys
252 import sys
253 import mimetypes
253 import mimetypes
254 import mimetools
254 import mimetools
255
255
256 class FTPRangeHandler(urllib2.FTPHandler):
256 class FTPRangeHandler(urllib2.FTPHandler):
257 def ftp_open(self, req):
257 def ftp_open(self, req):
258 host = req.get_host()
258 host = req.get_host()
259 if not host:
259 if not host:
260 raise IOError, ('ftp error', 'no host given')
260 raise IOError, ('ftp error', 'no host given')
261 host, port = splitport(host)
261 host, port = splitport(host)
262 if port is None:
262 if port is None:
263 port = ftplib.FTP_PORT
263 port = ftplib.FTP_PORT
264
264
265 # username/password handling
265 # username/password handling
266 user, host = splituser(host)
266 user, host = splituser(host)
267 if user:
267 if user:
268 user, passwd = splitpasswd(user)
268 user, passwd = splitpasswd(user)
269 else:
269 else:
270 passwd = None
270 passwd = None
271 host = unquote(host)
271 host = unquote(host)
272 user = unquote(user or '')
272 user = unquote(user or '')
273 passwd = unquote(passwd or '')
273 passwd = unquote(passwd or '')
274
274
275 try:
275 try:
276 host = socket.gethostbyname(host)
276 host = socket.gethostbyname(host)
277 except socket.error, msg:
277 except socket.error, msg:
278 raise urllib2.URLError(msg)
278 raise urllib2.URLError(msg)
279 path, attrs = splitattr(req.get_selector())
279 path, attrs = splitattr(req.get_selector())
280 dirs = path.split('/')
280 dirs = path.split('/')
281 dirs = map(unquote, dirs)
281 dirs = map(unquote, dirs)
282 dirs, file = dirs[:-1], dirs[-1]
282 dirs, file = dirs[:-1], dirs[-1]
283 if dirs and not dirs[0]:
283 if dirs and not dirs[0]:
284 dirs = dirs[1:]
284 dirs = dirs[1:]
285 try:
285 try:
286 fw = self.connect_ftp(user, passwd, host, port, dirs)
286 fw = self.connect_ftp(user, passwd, host, port, dirs)
287 type = file and 'I' or 'D'
287 type = file and 'I' or 'D'
288 for attr in attrs:
288 for attr in attrs:
289 attr, value = splitattr(attr)
289 attr, value = splitattr(attr)
290 if attr.lower() == 'type' and \
290 if attr.lower() == 'type' and \
291 value in ('a', 'A', 'i', 'I', 'd', 'D'):
291 value in ('a', 'A', 'i', 'I', 'd', 'D'):
292 type = value.upper()
292 type = value.upper()
293
293
294 # -- range support modifications start here
294 # -- range support modifications start here
295 rest = None
295 rest = None
296 range_tup = range_header_to_tuple(req.headers.get('Range', None))
296 range_tup = range_header_to_tuple(req.headers.get('Range', None))
297 assert range_tup != ()
297 assert range_tup != ()
298 if range_tup:
298 if range_tup:
299 (fb, lb) = range_tup
299 (fb, lb) = range_tup
300 if fb > 0:
300 if fb > 0:
301 rest = fb
301 rest = fb
302 # -- range support modifications end here
302 # -- range support modifications end here
303
303
304 fp, retrlen = fw.retrfile(file, type, rest)
304 fp, retrlen = fw.retrfile(file, type, rest)
305
305
306 # -- range support modifications start here
306 # -- range support modifications start here
307 if range_tup:
307 if range_tup:
308 (fb, lb) = range_tup
308 (fb, lb) = range_tup
309 if lb == '':
309 if lb == '':
310 if retrlen is None or retrlen == 0:
310 if retrlen is None or retrlen == 0:
311 raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.')
311 raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.')
312 lb = retrlen
312 lb = retrlen
313 retrlen = lb - fb
313 retrlen = lb - fb
314 if retrlen < 0:
314 if retrlen < 0:
315 # beginning of range is larger than file
315 # beginning of range is larger than file
316 raise RangeError('Requested Range Not Satisfiable')
316 raise RangeError('Requested Range Not Satisfiable')
317 else:
317 else:
318 retrlen = lb - fb
318 retrlen = lb - fb
319 fp = RangeableFileObject(fp, (0, retrlen))
319 fp = RangeableFileObject(fp, (0, retrlen))
320 # -- range support modifications end here
320 # -- range support modifications end here
321
321
322 headers = ""
322 headers = ""
323 mtype = mimetypes.guess_type(req.get_full_url())[0]
323 mtype = mimetypes.guess_type(req.get_full_url())[0]
324 if mtype:
324 if mtype:
325 headers += "Content-Type: %s\n" % mtype
325 headers += "Content-Type: %s\n" % mtype
326 if retrlen is not None and retrlen >= 0:
326 if retrlen is not None and retrlen >= 0:
327 headers += "Content-Length: %d\n" % retrlen
327 headers += "Content-Length: %d\n" % retrlen
328 sf = StringIO(headers)
328 sf = StringIO(headers)
329 headers = mimetools.Message(sf)
329 headers = mimetools.Message(sf)
330 return addinfourl(fp, headers, req.get_full_url())
330 return addinfourl(fp, headers, req.get_full_url())
331 except ftplib.all_errors, msg:
331 except ftplib.all_errors, msg:
332 raise IOError, ('ftp error', msg), sys.exc_info()[2]
332 raise IOError, ('ftp error', msg), sys.exc_info()[2]
333
333
334 def connect_ftp(self, user, passwd, host, port, dirs):
334 def connect_ftp(self, user, passwd, host, port, dirs):
335 fw = ftpwrapper(user, passwd, host, port, dirs)
335 fw = ftpwrapper(user, passwd, host, port, dirs)
336 return fw
336 return fw
337
337
338 class ftpwrapper(urllib.ftpwrapper):
338 class ftpwrapper(urllib.ftpwrapper):
339 # range support note:
339 # range support note:
340 # this ftpwrapper code is copied directly from
340 # this ftpwrapper code is copied directly from
341 # urllib. The only enhancement is to add the rest
341 # urllib. The only enhancement is to add the rest
342 # argument and pass it on to ftp.ntransfercmd
342 # argument and pass it on to ftp.ntransfercmd
343 def retrfile(self, file, type, rest=None):
343 def retrfile(self, file, type, rest=None):
344 self.endtransfer()
344 self.endtransfer()
345 if type in ('d', 'D'):
345 if type in ('d', 'D'):
346 cmd = 'TYPE A'
346 cmd = 'TYPE A'
347 isdir = 1
347 isdir = 1
348 else:
348 else:
349 cmd = 'TYPE ' + type
349 cmd = 'TYPE ' + type
350 isdir = 0
350 isdir = 0
351 try:
351 try:
352 self.ftp.voidcmd(cmd)
352 self.ftp.voidcmd(cmd)
353 except ftplib.all_errors:
353 except ftplib.all_errors:
354 self.init()
354 self.init()
355 self.ftp.voidcmd(cmd)
355 self.ftp.voidcmd(cmd)
356 conn = None
356 conn = None
357 if file and not isdir:
357 if file and not isdir:
358 # Use nlst to see if the file exists at all
358 # Use nlst to see if the file exists at all
359 try:
359 try:
360 self.ftp.nlst(file)
360 self.ftp.nlst(file)
361 except ftplib.error_perm, reason:
361 except ftplib.error_perm, reason:
362 raise IOError, ('ftp error', reason), sys.exc_info()[2]
362 raise IOError, ('ftp error', reason), sys.exc_info()[2]
363 # Restore the transfer mode!
363 # Restore the transfer mode!
364 self.ftp.voidcmd(cmd)
364 self.ftp.voidcmd(cmd)
365 # Try to retrieve as a file
365 # Try to retrieve as a file
366 try:
366 try:
367 cmd = 'RETR ' + file
367 cmd = 'RETR ' + file
368 conn = self.ftp.ntransfercmd(cmd, rest)
368 conn = self.ftp.ntransfercmd(cmd, rest)
369 except ftplib.error_perm, reason:
369 except ftplib.error_perm, reason:
370 if str(reason).startswith('501'):
370 if str(reason).startswith('501'):
371 # workaround for REST not supported error
371 # workaround for REST not supported error
372 fp, retrlen = self.retrfile(file, type)
372 fp, retrlen = self.retrfile(file, type)
373 fp = RangeableFileObject(fp, (rest,''))
373 fp = RangeableFileObject(fp, (rest,''))
374 return (fp, retrlen)
374 return (fp, retrlen)
375 elif not str(reason).startswith('550'):
375 elif not str(reason).startswith('550'):
376 raise IOError, ('ftp error', reason), sys.exc_info()[2]
376 raise IOError, ('ftp error', reason), sys.exc_info()[2]
377 if not conn:
377 if not conn:
378 # Set transfer mode to ASCII!
378 # Set transfer mode to ASCII!
379 self.ftp.voidcmd('TYPE A')
379 self.ftp.voidcmd('TYPE A')
380 # Try a directory listing
380 # Try a directory listing
381 if file:
381 if file:
382 cmd = 'LIST ' + file
382 cmd = 'LIST ' + file
383 else:
383 else:
384 cmd = 'LIST'
384 cmd = 'LIST'
385 conn = self.ftp.ntransfercmd(cmd)
385 conn = self.ftp.ntransfercmd(cmd)
386 self.busy = 1
386 self.busy = 1
387 # Pass back both a suitably decorated object and a retrieval length
387 # Pass back both a suitably decorated object and a retrieval length
388 return (addclosehook(conn[0].makefile('rb'),
388 return (addclosehook(conn[0].makefile('rb'),
389 self.endtransfer), conn[1])
389 self.endtransfer), conn[1])
390
390
391
391
392 ####################################################################
392 ####################################################################
393 # Range Tuple Functions
393 # Range Tuple Functions
394 # XXX: These range tuple functions might go better in a class.
394 # XXX: These range tuple functions might go better in a class.
395
395
396 _rangere = None
396 _rangere = None
397 def range_header_to_tuple(range_header):
397 def range_header_to_tuple(range_header):
398 """Get a (firstbyte,lastbyte) tuple from a Range header value.
398 """Get a (firstbyte,lastbyte) tuple from a Range header value.
399
399
400 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
400 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
401 function pulls the firstbyte and lastbyte values and returns
401 function pulls the firstbyte and lastbyte values and returns
402 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
402 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
403 the header value, it is returned as an empty string in the
403 the header value, it is returned as an empty string in the
404 tuple.
404 tuple.
405
405
406 Return None if range_header is None
406 Return None if range_header is None
407 Return () if range_header does not conform to the range spec
407 Return () if range_header does not conform to the range spec
408 pattern.
408 pattern.
409
409
410 """
410 """
411 global _rangere
411 global _rangere
412 if range_header is None:
412 if range_header is None:
413 return None
413 return None
414 if _rangere is None:
414 if _rangere is None:
415 import re
415 import re
416 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
416 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
417 match = _rangere.match(range_header)
417 match = _rangere.match(range_header)
418 if match:
418 if match:
419 tup = range_tuple_normalize(match.group(1, 2))
419 tup = range_tuple_normalize(match.group(1, 2))
420 if tup and tup[1]:
420 if tup and tup[1]:
421 tup = (tup[0], tup[1]+1)
421 tup = (tup[0], tup[1]+1)
422 return tup
422 return tup
423 return ()
423 return ()
424
424
425 def range_tuple_to_header(range_tup):
425 def range_tuple_to_header(range_tup):
426 """Convert a range tuple to a Range header value.
426 """Convert a range tuple to a Range header value.
427 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
427 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
428 if no range is needed.
428 if no range is needed.
429 """
429 """
430 if range_tup is None:
430 if range_tup is None:
431 return None
431 return None
432 range_tup = range_tuple_normalize(range_tup)
432 range_tup = range_tuple_normalize(range_tup)
433 if range_tup:
433 if range_tup:
434 if range_tup[1]:
434 if range_tup[1]:
435 range_tup = (range_tup[0], range_tup[1] - 1)
435 range_tup = (range_tup[0], range_tup[1] - 1)
436 return 'bytes=%s-%s' % range_tup
436 return 'bytes=%s-%s' % range_tup
437
437
438 def range_tuple_normalize(range_tup):
438 def range_tuple_normalize(range_tup):
439 """Normalize a (first_byte,last_byte) range tuple.
439 """Normalize a (first_byte,last_byte) range tuple.
440 Return a tuple whose first element is guaranteed to be an int
440 Return a tuple whose first element is guaranteed to be an int
441 and whose second element will be '' (meaning: the last byte) or
441 and whose second element will be '' (meaning: the last byte) or
442 an int. Finally, return None if the normalized tuple == (0,'')
442 an int. Finally, return None if the normalized tuple == (0,'')
443 as that is equivelant to retrieving the entire file.
443 as that is equivelant to retrieving the entire file.
444 """
444 """
445 if range_tup is None:
445 if range_tup is None:
446 return None
446 return None
447 # handle first byte
447 # handle first byte
448 fb = range_tup[0]
448 fb = range_tup[0]
449 if fb in (None, ''):
449 if fb in (None, ''):
450 fb = 0
450 fb = 0
451 else:
451 else:
452 fb = int(fb)
452 fb = int(fb)
453 # handle last byte
453 # handle last byte
454 try:
454 try:
455 lb = range_tup[1]
455 lb = range_tup[1]
456 except IndexError:
456 except IndexError:
457 lb = ''
457 lb = ''
458 else:
458 else:
459 if lb is None:
459 if lb is None:
460 lb = ''
460 lb = ''
461 elif lb != '':
461 elif lb != '':
462 lb = int(lb)
462 lb = int(lb)
463 # check if range is over the entire file
463 # check if range is over the entire file
464 if (fb, lb) == (0, ''):
464 if (fb, lb) == (0, ''):
465 return None
465 return None
466 # check that the range is valid
466 # check that the range is valid
467 if lb < fb:
467 if lb < fb:
468 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
468 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
469 return (fb, lb)
469 return (fb, lb)
@@ -1,108 +1,110
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 class ErrorResponse(Exception):
11 class ErrorResponse(Exception):
12 def __init__(self, code, message=None):
12 def __init__(self, code, message=None):
13 Exception.__init__(self)
13 Exception.__init__(self)
14 self.code = code
14 self.code = code
15 if message:
15 if message:
16 self.message = message
16 self.message = message
17 else:
17 else:
18 self.message = _statusmessage(code)
18 self.message = _statusmessage(code)
19
19
20 def _statusmessage(code):
20 def _statusmessage(code):
21 from BaseHTTPServer import BaseHTTPRequestHandler
21 from BaseHTTPServer import BaseHTTPRequestHandler
22 responses = BaseHTTPRequestHandler.responses
22 responses = BaseHTTPRequestHandler.responses
23 return responses.get(code, ('Error', 'Unknown error'))[0]
23 return responses.get(code, ('Error', 'Unknown error'))[0]
24
24
25 def statusmessage(code):
25 def statusmessage(code):
26 return '%d %s' % (code, _statusmessage(code))
26 return '%d %s' % (code, _statusmessage(code))
27
27
28 def get_mtime(repo_path):
28 def get_mtime(repo_path):
29 store_path = os.path.join(repo_path, ".hg")
29 store_path = os.path.join(repo_path, ".hg")
30 if not os.path.isdir(os.path.join(store_path, "data")):
30 if not os.path.isdir(os.path.join(store_path, "data")):
31 store_path = os.path.join(store_path, "store")
31 store_path = os.path.join(store_path, "store")
32 cl_path = os.path.join(store_path, "00changelog.i")
32 cl_path = os.path.join(store_path, "00changelog.i")
33 if os.path.exists(cl_path):
33 if os.path.exists(cl_path):
34 return os.stat(cl_path).st_mtime
34 return os.stat(cl_path).st_mtime
35 else:
35 else:
36 return os.stat(store_path).st_mtime
36 return os.stat(store_path).st_mtime
37
37
38 def staticfile(directory, fname, req):
38 def staticfile(directory, fname, req):
39 """return a file inside directory with guessed content-type header
39 """return a file inside directory with guessed Content-Type header
40
40
41 fname always uses '/' as directory separator and isn't allowed to
41 fname always uses '/' as directory separator and isn't allowed to
42 contain unusual path components.
42 contain unusual path components.
43 Content-type is guessed using the mimetypes module.
43 Content-Type is guessed using the mimetypes module.
44 Return an empty string if fname is illegal or file not found.
44 Return an empty string if fname is illegal or file not found.
45
45
46 """
46 """
47 parts = fname.split('/')
47 parts = fname.split('/')
48 path = directory
48 path = directory
49 for part in parts:
49 for part in parts:
50 if (part in ('', os.curdir, os.pardir) or
50 if (part in ('', os.curdir, os.pardir) or
51 os.sep in part or os.altsep is not None and os.altsep in part):
51 os.sep in part or os.altsep is not None and os.altsep in part):
52 return ""
52 return ""
53 path = os.path.join(path, part)
53 path = os.path.join(path, part)
54 try:
54 try:
55 os.stat(path)
55 os.stat(path)
56 ct = mimetypes.guess_type(path)[0] or "text/plain"
56 ct = mimetypes.guess_type(path)[0] or "text/plain"
57 req.header([('Content-type', ct),
57 req.header([
58 ('Content-length', str(os.path.getsize(path)))])
58 ('Content-Type', ct),
59 ('Content-Length', str(os.path.getsize(path)))
60 ])
59 return file(path, 'rb').read()
61 return file(path, 'rb').read()
60 except TypeError:
62 except TypeError:
61 raise ErrorResponse(500, 'illegal file name')
63 raise ErrorResponse(500, 'illegal file name')
62 except OSError, err:
64 except OSError, err:
63 if err.errno == errno.ENOENT:
65 if err.errno == errno.ENOENT:
64 raise ErrorResponse(404)
66 raise ErrorResponse(404)
65 else:
67 else:
66 raise ErrorResponse(500, err.strerror)
68 raise ErrorResponse(500, err.strerror)
67
69
68 def style_map(templatepath, style):
70 def style_map(templatepath, style):
69 """Return path to mapfile for a given style.
71 """Return path to mapfile for a given style.
70
72
71 Searches mapfile in the following locations:
73 Searches mapfile in the following locations:
72 1. templatepath/style/map
74 1. templatepath/style/map
73 2. templatepath/map-style
75 2. templatepath/map-style
74 3. templatepath/map
76 3. templatepath/map
75 """
77 """
76 locations = style and [os.path.join(style, "map"), "map-"+style] or []
78 locations = style and [os.path.join(style, "map"), "map-"+style] or []
77 locations.append("map")
79 locations.append("map")
78 for location in locations:
80 for location in locations:
79 mapfile = os.path.join(templatepath, location)
81 mapfile = os.path.join(templatepath, location)
80 if os.path.isfile(mapfile):
82 if os.path.isfile(mapfile):
81 return mapfile
83 return mapfile
82 raise RuntimeError("No hgweb templates found in %r" % templatepath)
84 raise RuntimeError("No hgweb templates found in %r" % templatepath)
83
85
84 def paritygen(stripecount, offset=0):
86 def paritygen(stripecount, offset=0):
85 """count parity of horizontal stripes for easier reading"""
87 """count parity of horizontal stripes for easier reading"""
86 if stripecount and offset:
88 if stripecount and offset:
87 # account for offset, e.g. due to building the list in reverse
89 # account for offset, e.g. due to building the list in reverse
88 count = (stripecount + offset) % stripecount
90 count = (stripecount + offset) % stripecount
89 parity = (stripecount + offset) / stripecount & 1
91 parity = (stripecount + offset) / stripecount & 1
90 else:
92 else:
91 count = 0
93 count = 0
92 parity = 0
94 parity = 0
93 while True:
95 while True:
94 yield parity
96 yield parity
95 count += 1
97 count += 1
96 if stripecount and count >= stripecount:
98 if stripecount and count >= stripecount:
97 parity = 1 - parity
99 parity = 1 - parity
98 count = 0
100 count = 0
99
101
100 def get_contact(config):
102 def get_contact(config):
101 """Return repo contact information or empty string.
103 """Return repo contact information or empty string.
102
104
103 web.contact is the primary source, but if that is not set, try
105 web.contact is the primary source, but if that is not set, try
104 ui.username or $EMAIL as a fallback to display something useful.
106 ui.username or $EMAIL as a fallback to display something useful.
105 """
107 """
106 return (config("web", "contact") or
108 return (config("web", "contact") or
107 config("ui", "username") or
109 config("ui", "username") or
108 os.environ.get("EMAIL") or "")
110 os.environ.get("EMAIL") or "")
@@ -1,906 +1,908
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, mimetypes, re
9 import os, mimetypes, re
10 from mercurial.node import *
10 from mercurial.node import *
11 from mercurial import mdiff, ui, hg, util, archival, patch, hook
11 from mercurial import mdiff, ui, hg, util, archival, patch, hook
12 from mercurial import revlog, templater
12 from mercurial import revlog, templater
13 from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact
13 from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact
14 from request import wsgirequest
14 from request import wsgirequest
15 import webcommands, protocol
15 import webcommands, protocol
16
16
17 shortcuts = {
17 shortcuts = {
18 'cl': [('cmd', ['changelog']), ('rev', None)],
18 'cl': [('cmd', ['changelog']), ('rev', None)],
19 'sl': [('cmd', ['shortlog']), ('rev', None)],
19 'sl': [('cmd', ['shortlog']), ('rev', None)],
20 'cs': [('cmd', ['changeset']), ('node', None)],
20 'cs': [('cmd', ['changeset']), ('node', None)],
21 'f': [('cmd', ['file']), ('filenode', None)],
21 'f': [('cmd', ['file']), ('filenode', None)],
22 'fl': [('cmd', ['filelog']), ('filenode', None)],
22 'fl': [('cmd', ['filelog']), ('filenode', None)],
23 'fd': [('cmd', ['filediff']), ('node', None)],
23 'fd': [('cmd', ['filediff']), ('node', None)],
24 'fa': [('cmd', ['annotate']), ('filenode', None)],
24 'fa': [('cmd', ['annotate']), ('filenode', None)],
25 'mf': [('cmd', ['manifest']), ('manifest', None)],
25 'mf': [('cmd', ['manifest']), ('manifest', None)],
26 'ca': [('cmd', ['archive']), ('node', None)],
26 'ca': [('cmd', ['archive']), ('node', None)],
27 'tags': [('cmd', ['tags'])],
27 'tags': [('cmd', ['tags'])],
28 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
28 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
29 'static': [('cmd', ['static']), ('file', None)]
29 'static': [('cmd', ['static']), ('file', None)]
30 }
30 }
31
31
32 def _up(p):
32 def _up(p):
33 if p[0] != "/":
33 if p[0] != "/":
34 p = "/" + p
34 p = "/" + p
35 if p[-1] == "/":
35 if p[-1] == "/":
36 p = p[:-1]
36 p = p[:-1]
37 up = os.path.dirname(p)
37 up = os.path.dirname(p)
38 if up == "/":
38 if up == "/":
39 return "/"
39 return "/"
40 return up + "/"
40 return up + "/"
41
41
42 def revnavgen(pos, pagelen, limit, nodefunc):
42 def revnavgen(pos, pagelen, limit, nodefunc):
43 def seq(factor, limit=None):
43 def seq(factor, limit=None):
44 if limit:
44 if limit:
45 yield limit
45 yield limit
46 if limit >= 20 and limit <= 40:
46 if limit >= 20 and limit <= 40:
47 yield 50
47 yield 50
48 else:
48 else:
49 yield 1 * factor
49 yield 1 * factor
50 yield 3 * factor
50 yield 3 * factor
51 for f in seq(factor * 10):
51 for f in seq(factor * 10):
52 yield f
52 yield f
53
53
54 def nav(**map):
54 def nav(**map):
55 l = []
55 l = []
56 last = 0
56 last = 0
57 for f in seq(1, pagelen):
57 for f in seq(1, pagelen):
58 if f < pagelen or f <= last:
58 if f < pagelen or f <= last:
59 continue
59 continue
60 if f > limit:
60 if f > limit:
61 break
61 break
62 last = f
62 last = f
63 if pos + f < limit:
63 if pos + f < limit:
64 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
64 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
65 if pos - f >= 0:
65 if pos - f >= 0:
66 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
66 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
67
67
68 try:
68 try:
69 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
69 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
70
70
71 for label, node in l:
71 for label, node in l:
72 yield {"label": label, "node": node}
72 yield {"label": label, "node": node}
73
73
74 yield {"label": "tip", "node": "tip"}
74 yield {"label": "tip", "node": "tip"}
75 except hg.RepoError:
75 except hg.RepoError:
76 pass
76 pass
77
77
78 return nav
78 return nav
79
79
80 class hgweb(object):
80 class hgweb(object):
81 def __init__(self, repo, name=None):
81 def __init__(self, repo, name=None):
82 if isinstance(repo, str):
82 if isinstance(repo, str):
83 parentui = ui.ui(report_untrusted=False, interactive=False)
83 parentui = ui.ui(report_untrusted=False, interactive=False)
84 self.repo = hg.repository(parentui, repo)
84 self.repo = hg.repository(parentui, repo)
85 else:
85 else:
86 self.repo = repo
86 self.repo = repo
87
87
88 hook.redirect(True)
88 hook.redirect(True)
89 self.mtime = -1
89 self.mtime = -1
90 self.reponame = name
90 self.reponame = name
91 self.archives = 'zip', 'gz', 'bz2'
91 self.archives = 'zip', 'gz', 'bz2'
92 self.stripecount = 1
92 self.stripecount = 1
93 # a repo owner may set web.templates in .hg/hgrc to get any file
93 # a repo owner may set web.templates in .hg/hgrc to get any file
94 # readable by the user running the CGI script
94 # readable by the user running the CGI script
95 self.templatepath = self.config("web", "templates",
95 self.templatepath = self.config("web", "templates",
96 templater.templatepath(),
96 templater.templatepath(),
97 untrusted=False)
97 untrusted=False)
98
98
99 # The CGI scripts are often run by a user different from the repo owner.
99 # The CGI scripts are often run by a user different from the repo owner.
100 # Trust the settings from the .hg/hgrc files by default.
100 # Trust the settings from the .hg/hgrc files by default.
101 def config(self, section, name, default=None, untrusted=True):
101 def config(self, section, name, default=None, untrusted=True):
102 return self.repo.ui.config(section, name, default,
102 return self.repo.ui.config(section, name, default,
103 untrusted=untrusted)
103 untrusted=untrusted)
104
104
105 def configbool(self, section, name, default=False, untrusted=True):
105 def configbool(self, section, name, default=False, untrusted=True):
106 return self.repo.ui.configbool(section, name, default,
106 return self.repo.ui.configbool(section, name, default,
107 untrusted=untrusted)
107 untrusted=untrusted)
108
108
109 def configlist(self, section, name, default=None, untrusted=True):
109 def configlist(self, section, name, default=None, untrusted=True):
110 return self.repo.ui.configlist(section, name, default,
110 return self.repo.ui.configlist(section, name, default,
111 untrusted=untrusted)
111 untrusted=untrusted)
112
112
113 def refresh(self):
113 def refresh(self):
114 mtime = get_mtime(self.repo.root)
114 mtime = get_mtime(self.repo.root)
115 if mtime != self.mtime:
115 if mtime != self.mtime:
116 self.mtime = mtime
116 self.mtime = mtime
117 self.repo = hg.repository(self.repo.ui, self.repo.root)
117 self.repo = hg.repository(self.repo.ui, self.repo.root)
118 self.maxchanges = int(self.config("web", "maxchanges", 10))
118 self.maxchanges = int(self.config("web", "maxchanges", 10))
119 self.stripecount = int(self.config("web", "stripes", 1))
119 self.stripecount = int(self.config("web", "stripes", 1))
120 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
120 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
121 self.maxfiles = int(self.config("web", "maxfiles", 10))
121 self.maxfiles = int(self.config("web", "maxfiles", 10))
122 self.allowpull = self.configbool("web", "allowpull", True)
122 self.allowpull = self.configbool("web", "allowpull", True)
123 self.encoding = self.config("web", "encoding", util._encoding)
123 self.encoding = self.config("web", "encoding", util._encoding)
124
124
125 def run(self):
125 def run(self):
126 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
126 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
127 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
127 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
128 import mercurial.hgweb.wsgicgi as wsgicgi
128 import mercurial.hgweb.wsgicgi as wsgicgi
129 wsgicgi.launch(self)
129 wsgicgi.launch(self)
130
130
131 def __call__(self, env, respond):
131 def __call__(self, env, respond):
132 req = wsgirequest(env, respond)
132 req = wsgirequest(env, respond)
133 self.run_wsgi(req)
133 self.run_wsgi(req)
134 return req
134 return req
135
135
136 def run_wsgi(self, req):
136 def run_wsgi(self, req):
137
137
138 self.refresh()
138 self.refresh()
139
139
140 # expand form shortcuts
140 # expand form shortcuts
141
141
142 for k in shortcuts.iterkeys():
142 for k in shortcuts.iterkeys():
143 if k in req.form:
143 if k in req.form:
144 for name, value in shortcuts[k]:
144 for name, value in shortcuts[k]:
145 if value is None:
145 if value is None:
146 value = req.form[k]
146 value = req.form[k]
147 req.form[name] = value
147 req.form[name] = value
148 del req.form[k]
148 del req.form[k]
149
149
150 # work with CGI variables to create coherent structure
150 # work with CGI variables to create coherent structure
151 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
151 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
152
152
153 req.url = req.env['SCRIPT_NAME']
153 req.url = req.env['SCRIPT_NAME']
154 if not req.url.endswith('/'):
154 if not req.url.endswith('/'):
155 req.url += '/'
155 req.url += '/'
156 if 'REPO_NAME' in req.env:
156 if 'REPO_NAME' in req.env:
157 req.url += req.env['REPO_NAME'] + '/'
157 req.url += req.env['REPO_NAME'] + '/'
158
158
159 if req.env.get('PATH_INFO'):
159 if req.env.get('PATH_INFO'):
160 parts = req.env.get('PATH_INFO').strip('/').split('/')
160 parts = req.env.get('PATH_INFO').strip('/').split('/')
161 repo_parts = req.env.get('REPO_NAME', '').split('/')
161 repo_parts = req.env.get('REPO_NAME', '').split('/')
162 if parts[:len(repo_parts)] == repo_parts:
162 if parts[:len(repo_parts)] == repo_parts:
163 parts = parts[len(repo_parts):]
163 parts = parts[len(repo_parts):]
164 query = '/'.join(parts)
164 query = '/'.join(parts)
165 else:
165 else:
166 query = req.env['QUERY_STRING'].split('&', 1)[0]
166 query = req.env['QUERY_STRING'].split('&', 1)[0]
167 query = query.split(';', 1)[0]
167 query = query.split(';', 1)[0]
168
168
169 # translate user-visible url structure to internal structure
169 # translate user-visible url structure to internal structure
170
170
171 args = query.split('/', 2)
171 args = query.split('/', 2)
172 if 'cmd' not in req.form and args and args[0]:
172 if 'cmd' not in req.form and args and args[0]:
173
173
174 cmd = args.pop(0)
174 cmd = args.pop(0)
175 style = cmd.rfind('-')
175 style = cmd.rfind('-')
176 if style != -1:
176 if style != -1:
177 req.form['style'] = [cmd[:style]]
177 req.form['style'] = [cmd[:style]]
178 cmd = cmd[style+1:]
178 cmd = cmd[style+1:]
179
179
180 # avoid accepting e.g. style parameter as command
180 # avoid accepting e.g. style parameter as command
181 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
181 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
182 req.form['cmd'] = [cmd]
182 req.form['cmd'] = [cmd]
183
183
184 if args and args[0]:
184 if args and args[0]:
185 node = args.pop(0)
185 node = args.pop(0)
186 req.form['node'] = [node]
186 req.form['node'] = [node]
187 if args:
187 if args:
188 req.form['file'] = args
188 req.form['file'] = args
189
189
190 if cmd == 'static':
190 if cmd == 'static':
191 req.form['file'] = req.form['node']
191 req.form['file'] = req.form['node']
192 elif cmd == 'archive':
192 elif cmd == 'archive':
193 fn = req.form['node'][0]
193 fn = req.form['node'][0]
194 for type_, spec in self.archive_specs.iteritems():
194 for type_, spec in self.archive_specs.iteritems():
195 ext = spec[2]
195 ext = spec[2]
196 if fn.endswith(ext):
196 if fn.endswith(ext):
197 req.form['node'] = [fn[:-len(ext)]]
197 req.form['node'] = [fn[:-len(ext)]]
198 req.form['type'] = [type_]
198 req.form['type'] = [type_]
199
199
200 # actually process the request
200 # actually process the request
201
201
202 try:
202 try:
203
203
204 cmd = req.form.get('cmd', [''])[0]
204 cmd = req.form.get('cmd', [''])[0]
205 if hasattr(protocol, cmd):
205 if hasattr(protocol, cmd):
206 method = getattr(protocol, cmd)
206 method = getattr(protocol, cmd)
207 method(self, req)
207 method(self, req)
208 else:
208 else:
209
209
210 tmpl = self.templater(req)
210 tmpl = self.templater(req)
211 if cmd == '':
211 if cmd == '':
212 req.form['cmd'] = [tmpl.cache['default']]
212 req.form['cmd'] = [tmpl.cache['default']]
213 cmd = req.form['cmd'][0]
213 cmd = req.form['cmd'][0]
214
214
215 if cmd == 'file' and 'raw' in req.form.get('style', []):
215 if cmd == 'file' and 'raw' in req.form.get('style', []):
216 webcommands.rawfile(self, req, tmpl)
216 webcommands.rawfile(self, req, tmpl)
217 else:
217 else:
218 getattr(webcommands, cmd)(self, req, tmpl)
218 getattr(webcommands, cmd)(self, req, tmpl)
219
219
220 del tmpl
220 del tmpl
221
221
222 except revlog.LookupError, err:
222 except revlog.LookupError, err:
223 req.respond(404, tmpl(
223 req.respond(404, tmpl(
224 'error', error='revision not found: %s' % err.name))
224 'error', error='revision not found: %s' % err.name))
225 except (hg.RepoError, revlog.RevlogError), inst:
225 except (hg.RepoError, revlog.RevlogError), inst:
226 req.respond('500 Internal Server Error',
226 req.respond('500 Internal Server Error',
227 tmpl('error', error=str(inst)))
227 tmpl('error', error=str(inst)))
228 except ErrorResponse, inst:
228 except ErrorResponse, inst:
229 req.respond(inst.code, tmpl('error', error=inst.message))
229 req.respond(inst.code, tmpl('error', error=inst.message))
230 except AttributeError:
230 except AttributeError:
231 req.respond(400, tmpl('error', error='No such method: ' + cmd))
231 req.respond(400, tmpl('error', error='No such method: ' + cmd))
232
232
233 def templater(self, req):
233 def templater(self, req):
234
234
235 # determine scheme, port and server name
235 # determine scheme, port and server name
236 # this is needed to create absolute urls
236 # this is needed to create absolute urls
237
237
238 proto = req.env.get('wsgi.url_scheme')
238 proto = req.env.get('wsgi.url_scheme')
239 if proto == 'https':
239 if proto == 'https':
240 proto = 'https'
240 proto = 'https'
241 default_port = "443"
241 default_port = "443"
242 else:
242 else:
243 proto = 'http'
243 proto = 'http'
244 default_port = "80"
244 default_port = "80"
245
245
246 port = req.env["SERVER_PORT"]
246 port = req.env["SERVER_PORT"]
247 port = port != default_port and (":" + port) or ""
247 port = port != default_port and (":" + port) or ""
248 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
248 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
249 staticurl = self.config("web", "staticurl") or req.url + 'static/'
249 staticurl = self.config("web", "staticurl") or req.url + 'static/'
250 if not staticurl.endswith('/'):
250 if not staticurl.endswith('/'):
251 staticurl += '/'
251 staticurl += '/'
252
252
253 # some functions for the templater
253 # some functions for the templater
254
254
255 def header(**map):
255 def header(**map):
256 ctype = tmpl('mimetype', encoding=self.encoding)
256 ctype = tmpl('mimetype', encoding=self.encoding)
257 req.httphdr(templater.stringify(ctype))
257 req.httphdr(templater.stringify(ctype))
258 yield tmpl('header', encoding=self.encoding, **map)
258 yield tmpl('header', encoding=self.encoding, **map)
259
259
260 def footer(**map):
260 def footer(**map):
261 yield tmpl("footer", **map)
261 yield tmpl("footer", **map)
262
262
263 def motd(**map):
263 def motd(**map):
264 yield self.config("web", "motd", "")
264 yield self.config("web", "motd", "")
265
265
266 def sessionvars(**map):
266 def sessionvars(**map):
267 fields = []
267 fields = []
268 if 'style' in req.form:
268 if 'style' in req.form:
269 style = req.form['style'][0]
269 style = req.form['style'][0]
270 if style != self.config('web', 'style', ''):
270 if style != self.config('web', 'style', ''):
271 fields.append(('style', style))
271 fields.append(('style', style))
272
272
273 separator = req.url[-1] == '?' and ';' or '?'
273 separator = req.url[-1] == '?' and ';' or '?'
274 for name, value in fields:
274 for name, value in fields:
275 yield dict(name=name, value=value, separator=separator)
275 yield dict(name=name, value=value, separator=separator)
276 separator = ';'
276 separator = ';'
277
277
278 # figure out which style to use
278 # figure out which style to use
279
279
280 style = self.config("web", "style", "")
280 style = self.config("web", "style", "")
281 if 'style' in req.form:
281 if 'style' in req.form:
282 style = req.form['style'][0]
282 style = req.form['style'][0]
283 mapfile = style_map(self.templatepath, style)
283 mapfile = style_map(self.templatepath, style)
284
284
285 if not self.reponame:
285 if not self.reponame:
286 self.reponame = (self.config("web", "name")
286 self.reponame = (self.config("web", "name")
287 or req.env.get('REPO_NAME')
287 or req.env.get('REPO_NAME')
288 or req.url.strip('/') or self.repo.root)
288 or req.url.strip('/') or self.repo.root)
289
289
290 # create the templater
290 # create the templater
291
291
292 tmpl = templater.templater(mapfile, templater.common_filters,
292 tmpl = templater.templater(mapfile, templater.common_filters,
293 defaults={"url": req.url,
293 defaults={"url": req.url,
294 "staticurl": staticurl,
294 "staticurl": staticurl,
295 "urlbase": urlbase,
295 "urlbase": urlbase,
296 "repo": self.reponame,
296 "repo": self.reponame,
297 "header": header,
297 "header": header,
298 "footer": footer,
298 "footer": footer,
299 "motd": motd,
299 "motd": motd,
300 "sessionvars": sessionvars
300 "sessionvars": sessionvars
301 })
301 })
302 return tmpl
302 return tmpl
303
303
304 def archivelist(self, nodeid):
304 def archivelist(self, nodeid):
305 allowed = self.configlist("web", "allow_archive")
305 allowed = self.configlist("web", "allow_archive")
306 for i, spec in self.archive_specs.iteritems():
306 for i, spec in self.archive_specs.iteritems():
307 if i in allowed or self.configbool("web", "allow" + i):
307 if i in allowed or self.configbool("web", "allow" + i):
308 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
308 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
309
309
310 def listfilediffs(self, tmpl, files, changeset):
310 def listfilediffs(self, tmpl, files, changeset):
311 for f in files[:self.maxfiles]:
311 for f in files[:self.maxfiles]:
312 yield tmpl("filedifflink", node=hex(changeset), file=f)
312 yield tmpl("filedifflink", node=hex(changeset), file=f)
313 if len(files) > self.maxfiles:
313 if len(files) > self.maxfiles:
314 yield tmpl("fileellipses")
314 yield tmpl("fileellipses")
315
315
316 def siblings(self, siblings=[], hiderev=None, **args):
316 def siblings(self, siblings=[], hiderev=None, **args):
317 siblings = [s for s in siblings if s.node() != nullid]
317 siblings = [s for s in siblings if s.node() != nullid]
318 if len(siblings) == 1 and siblings[0].rev() == hiderev:
318 if len(siblings) == 1 and siblings[0].rev() == hiderev:
319 return
319 return
320 for s in siblings:
320 for s in siblings:
321 d = {'node': hex(s.node()), 'rev': s.rev()}
321 d = {'node': hex(s.node()), 'rev': s.rev()}
322 if hasattr(s, 'path'):
322 if hasattr(s, 'path'):
323 d['file'] = s.path()
323 d['file'] = s.path()
324 d.update(args)
324 d.update(args)
325 yield d
325 yield d
326
326
327 def renamelink(self, fl, node):
327 def renamelink(self, fl, node):
328 r = fl.renamed(node)
328 r = fl.renamed(node)
329 if r:
329 if r:
330 return [dict(file=r[0], node=hex(r[1]))]
330 return [dict(file=r[0], node=hex(r[1]))]
331 return []
331 return []
332
332
333 def nodetagsdict(self, node):
333 def nodetagsdict(self, node):
334 return [{"name": i} for i in self.repo.nodetags(node)]
334 return [{"name": i} for i in self.repo.nodetags(node)]
335
335
336 def nodebranchdict(self, ctx):
336 def nodebranchdict(self, ctx):
337 branches = []
337 branches = []
338 branch = ctx.branch()
338 branch = ctx.branch()
339 # If this is an empty repo, ctx.node() == nullid,
339 # If this is an empty repo, ctx.node() == nullid,
340 # ctx.branch() == 'default', but branchtags() is
340 # ctx.branch() == 'default', but branchtags() is
341 # an empty dict. Using dict.get avoids a traceback.
341 # an empty dict. Using dict.get avoids a traceback.
342 if self.repo.branchtags().get(branch) == ctx.node():
342 if self.repo.branchtags().get(branch) == ctx.node():
343 branches.append({"name": branch})
343 branches.append({"name": branch})
344 return branches
344 return branches
345
345
346 def showtag(self, tmpl, t1, node=nullid, **args):
346 def showtag(self, tmpl, t1, node=nullid, **args):
347 for t in self.repo.nodetags(node):
347 for t in self.repo.nodetags(node):
348 yield tmpl(t1, tag=t, **args)
348 yield tmpl(t1, tag=t, **args)
349
349
350 def diff(self, tmpl, node1, node2, files):
350 def diff(self, tmpl, node1, node2, files):
351 def filterfiles(filters, files):
351 def filterfiles(filters, files):
352 l = [x for x in files if x in filters]
352 l = [x for x in files if x in filters]
353
353
354 for t in filters:
354 for t in filters:
355 if t and t[-1] != os.sep:
355 if t and t[-1] != os.sep:
356 t += os.sep
356 t += os.sep
357 l += [x for x in files if x.startswith(t)]
357 l += [x for x in files if x.startswith(t)]
358 return l
358 return l
359
359
360 parity = paritygen(self.stripecount)
360 parity = paritygen(self.stripecount)
361 def diffblock(diff, f, fn):
361 def diffblock(diff, f, fn):
362 yield tmpl("diffblock",
362 yield tmpl("diffblock",
363 lines=prettyprintlines(diff),
363 lines=prettyprintlines(diff),
364 parity=parity.next(),
364 parity=parity.next(),
365 file=f,
365 file=f,
366 filenode=hex(fn or nullid))
366 filenode=hex(fn or nullid))
367
367
368 def prettyprintlines(diff):
368 def prettyprintlines(diff):
369 for l in diff.splitlines(1):
369 for l in diff.splitlines(1):
370 if l.startswith('+'):
370 if l.startswith('+'):
371 yield tmpl("difflineplus", line=l)
371 yield tmpl("difflineplus", line=l)
372 elif l.startswith('-'):
372 elif l.startswith('-'):
373 yield tmpl("difflineminus", line=l)
373 yield tmpl("difflineminus", line=l)
374 elif l.startswith('@'):
374 elif l.startswith('@'):
375 yield tmpl("difflineat", line=l)
375 yield tmpl("difflineat", line=l)
376 else:
376 else:
377 yield tmpl("diffline", line=l)
377 yield tmpl("diffline", line=l)
378
378
379 r = self.repo
379 r = self.repo
380 c1 = r.changectx(node1)
380 c1 = r.changectx(node1)
381 c2 = r.changectx(node2)
381 c2 = r.changectx(node2)
382 date1 = util.datestr(c1.date())
382 date1 = util.datestr(c1.date())
383 date2 = util.datestr(c2.date())
383 date2 = util.datestr(c2.date())
384
384
385 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
385 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
386 if files:
386 if files:
387 modified, added, removed = map(lambda x: filterfiles(files, x),
387 modified, added, removed = map(lambda x: filterfiles(files, x),
388 (modified, added, removed))
388 (modified, added, removed))
389
389
390 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
390 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
391 for f in modified:
391 for f in modified:
392 to = c1.filectx(f).data()
392 to = c1.filectx(f).data()
393 tn = c2.filectx(f).data()
393 tn = c2.filectx(f).data()
394 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
394 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
395 opts=diffopts), f, tn)
395 opts=diffopts), f, tn)
396 for f in added:
396 for f in added:
397 to = None
397 to = None
398 tn = c2.filectx(f).data()
398 tn = c2.filectx(f).data()
399 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
399 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
400 opts=diffopts), f, tn)
400 opts=diffopts), f, tn)
401 for f in removed:
401 for f in removed:
402 to = c1.filectx(f).data()
402 to = c1.filectx(f).data()
403 tn = None
403 tn = None
404 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
404 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
405 opts=diffopts), f, tn)
405 opts=diffopts), f, tn)
406
406
407 def changelog(self, tmpl, ctx, shortlog=False):
407 def changelog(self, tmpl, ctx, shortlog=False):
408 def changelist(limit=0,**map):
408 def changelist(limit=0,**map):
409 cl = self.repo.changelog
409 cl = self.repo.changelog
410 l = [] # build a list in forward order for efficiency
410 l = [] # build a list in forward order for efficiency
411 for i in xrange(start, end):
411 for i in xrange(start, end):
412 ctx = self.repo.changectx(i)
412 ctx = self.repo.changectx(i)
413 n = ctx.node()
413 n = ctx.node()
414
414
415 l.insert(0, {"parity": parity.next(),
415 l.insert(0, {"parity": parity.next(),
416 "author": ctx.user(),
416 "author": ctx.user(),
417 "parent": self.siblings(ctx.parents(), i - 1),
417 "parent": self.siblings(ctx.parents(), i - 1),
418 "child": self.siblings(ctx.children(), i + 1),
418 "child": self.siblings(ctx.children(), i + 1),
419 "changelogtag": self.showtag("changelogtag",n),
419 "changelogtag": self.showtag("changelogtag",n),
420 "desc": ctx.description(),
420 "desc": ctx.description(),
421 "date": ctx.date(),
421 "date": ctx.date(),
422 "files": self.listfilediffs(tmpl, ctx.files(), n),
422 "files": self.listfilediffs(tmpl, ctx.files(), n),
423 "rev": i,
423 "rev": i,
424 "node": hex(n),
424 "node": hex(n),
425 "tags": self.nodetagsdict(n),
425 "tags": self.nodetagsdict(n),
426 "branches": self.nodebranchdict(ctx)})
426 "branches": self.nodebranchdict(ctx)})
427
427
428 if limit > 0:
428 if limit > 0:
429 l = l[:limit]
429 l = l[:limit]
430
430
431 for e in l:
431 for e in l:
432 yield e
432 yield e
433
433
434 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
434 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
435 cl = self.repo.changelog
435 cl = self.repo.changelog
436 count = cl.count()
436 count = cl.count()
437 pos = ctx.rev()
437 pos = ctx.rev()
438 start = max(0, pos - maxchanges + 1)
438 start = max(0, pos - maxchanges + 1)
439 end = min(count, start + maxchanges)
439 end = min(count, start + maxchanges)
440 pos = end - 1
440 pos = end - 1
441 parity = paritygen(self.stripecount, offset=start-end)
441 parity = paritygen(self.stripecount, offset=start-end)
442
442
443 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
443 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
444
444
445 return tmpl(shortlog and 'shortlog' or 'changelog',
445 return tmpl(shortlog and 'shortlog' or 'changelog',
446 changenav=changenav,
446 changenav=changenav,
447 node=hex(cl.tip()),
447 node=hex(cl.tip()),
448 rev=pos, changesets=count,
448 rev=pos, changesets=count,
449 entries=lambda **x: changelist(limit=0,**x),
449 entries=lambda **x: changelist(limit=0,**x),
450 latestentry=lambda **x: changelist(limit=1,**x),
450 latestentry=lambda **x: changelist(limit=1,**x),
451 archives=self.archivelist("tip"))
451 archives=self.archivelist("tip"))
452
452
453 def search(self, tmpl, query):
453 def search(self, tmpl, query):
454
454
455 def changelist(**map):
455 def changelist(**map):
456 cl = self.repo.changelog
456 cl = self.repo.changelog
457 count = 0
457 count = 0
458 qw = query.lower().split()
458 qw = query.lower().split()
459
459
460 def revgen():
460 def revgen():
461 for i in xrange(cl.count() - 1, 0, -100):
461 for i in xrange(cl.count() - 1, 0, -100):
462 l = []
462 l = []
463 for j in xrange(max(0, i - 100), i):
463 for j in xrange(max(0, i - 100), i):
464 ctx = self.repo.changectx(j)
464 ctx = self.repo.changectx(j)
465 l.append(ctx)
465 l.append(ctx)
466 l.reverse()
466 l.reverse()
467 for e in l:
467 for e in l:
468 yield e
468 yield e
469
469
470 for ctx in revgen():
470 for ctx in revgen():
471 miss = 0
471 miss = 0
472 for q in qw:
472 for q in qw:
473 if not (q in ctx.user().lower() or
473 if not (q in ctx.user().lower() or
474 q in ctx.description().lower() or
474 q in ctx.description().lower() or
475 q in " ".join(ctx.files()).lower()):
475 q in " ".join(ctx.files()).lower()):
476 miss = 1
476 miss = 1
477 break
477 break
478 if miss:
478 if miss:
479 continue
479 continue
480
480
481 count += 1
481 count += 1
482 n = ctx.node()
482 n = ctx.node()
483
483
484 yield tmpl('searchentry',
484 yield tmpl('searchentry',
485 parity=parity.next(),
485 parity=parity.next(),
486 author=ctx.user(),
486 author=ctx.user(),
487 parent=self.siblings(ctx.parents()),
487 parent=self.siblings(ctx.parents()),
488 child=self.siblings(ctx.children()),
488 child=self.siblings(ctx.children()),
489 changelogtag=self.showtag("changelogtag",n),
489 changelogtag=self.showtag("changelogtag",n),
490 desc=ctx.description(),
490 desc=ctx.description(),
491 date=ctx.date(),
491 date=ctx.date(),
492 files=self.listfilediffs(tmpl, ctx.files(), n),
492 files=self.listfilediffs(tmpl, ctx.files(), n),
493 rev=ctx.rev(),
493 rev=ctx.rev(),
494 node=hex(n),
494 node=hex(n),
495 tags=self.nodetagsdict(n),
495 tags=self.nodetagsdict(n),
496 branches=self.nodebranchdict(ctx))
496 branches=self.nodebranchdict(ctx))
497
497
498 if count >= self.maxchanges:
498 if count >= self.maxchanges:
499 break
499 break
500
500
501 cl = self.repo.changelog
501 cl = self.repo.changelog
502 parity = paritygen(self.stripecount)
502 parity = paritygen(self.stripecount)
503
503
504 return tmpl('search',
504 return tmpl('search',
505 query=query,
505 query=query,
506 node=hex(cl.tip()),
506 node=hex(cl.tip()),
507 entries=changelist,
507 entries=changelist,
508 archives=self.archivelist("tip"))
508 archives=self.archivelist("tip"))
509
509
510 def changeset(self, tmpl, ctx):
510 def changeset(self, tmpl, ctx):
511 n = ctx.node()
511 n = ctx.node()
512 parents = ctx.parents()
512 parents = ctx.parents()
513 p1 = parents[0].node()
513 p1 = parents[0].node()
514
514
515 files = []
515 files = []
516 parity = paritygen(self.stripecount)
516 parity = paritygen(self.stripecount)
517 for f in ctx.files():
517 for f in ctx.files():
518 files.append(tmpl("filenodelink",
518 files.append(tmpl("filenodelink",
519 node=hex(n), file=f,
519 node=hex(n), file=f,
520 parity=parity.next()))
520 parity=parity.next()))
521
521
522 def diff(**map):
522 def diff(**map):
523 yield self.diff(tmpl, p1, n, None)
523 yield self.diff(tmpl, p1, n, None)
524
524
525 return tmpl('changeset',
525 return tmpl('changeset',
526 diff=diff,
526 diff=diff,
527 rev=ctx.rev(),
527 rev=ctx.rev(),
528 node=hex(n),
528 node=hex(n),
529 parent=self.siblings(parents),
529 parent=self.siblings(parents),
530 child=self.siblings(ctx.children()),
530 child=self.siblings(ctx.children()),
531 changesettag=self.showtag("changesettag",n),
531 changesettag=self.showtag("changesettag",n),
532 author=ctx.user(),
532 author=ctx.user(),
533 desc=ctx.description(),
533 desc=ctx.description(),
534 date=ctx.date(),
534 date=ctx.date(),
535 files=files,
535 files=files,
536 archives=self.archivelist(hex(n)),
536 archives=self.archivelist(hex(n)),
537 tags=self.nodetagsdict(n),
537 tags=self.nodetagsdict(n),
538 branches=self.nodebranchdict(ctx))
538 branches=self.nodebranchdict(ctx))
539
539
540 def filelog(self, tmpl, fctx):
540 def filelog(self, tmpl, fctx):
541 f = fctx.path()
541 f = fctx.path()
542 fl = fctx.filelog()
542 fl = fctx.filelog()
543 count = fl.count()
543 count = fl.count()
544 pagelen = self.maxshortchanges
544 pagelen = self.maxshortchanges
545 pos = fctx.filerev()
545 pos = fctx.filerev()
546 start = max(0, pos - pagelen + 1)
546 start = max(0, pos - pagelen + 1)
547 end = min(count, start + pagelen)
547 end = min(count, start + pagelen)
548 pos = end - 1
548 pos = end - 1
549 parity = paritygen(self.stripecount, offset=start-end)
549 parity = paritygen(self.stripecount, offset=start-end)
550
550
551 def entries(limit=0, **map):
551 def entries(limit=0, **map):
552 l = []
552 l = []
553
553
554 for i in xrange(start, end):
554 for i in xrange(start, end):
555 ctx = fctx.filectx(i)
555 ctx = fctx.filectx(i)
556 n = fl.node(i)
556 n = fl.node(i)
557
557
558 l.insert(0, {"parity": parity.next(),
558 l.insert(0, {"parity": parity.next(),
559 "filerev": i,
559 "filerev": i,
560 "file": f,
560 "file": f,
561 "node": hex(ctx.node()),
561 "node": hex(ctx.node()),
562 "author": ctx.user(),
562 "author": ctx.user(),
563 "date": ctx.date(),
563 "date": ctx.date(),
564 "rename": self.renamelink(fl, n),
564 "rename": self.renamelink(fl, n),
565 "parent": self.siblings(fctx.parents()),
565 "parent": self.siblings(fctx.parents()),
566 "child": self.siblings(fctx.children()),
566 "child": self.siblings(fctx.children()),
567 "desc": ctx.description()})
567 "desc": ctx.description()})
568
568
569 if limit > 0:
569 if limit > 0:
570 l = l[:limit]
570 l = l[:limit]
571
571
572 for e in l:
572 for e in l:
573 yield e
573 yield e
574
574
575 nodefunc = lambda x: fctx.filectx(fileid=x)
575 nodefunc = lambda x: fctx.filectx(fileid=x)
576 nav = revnavgen(pos, pagelen, count, nodefunc)
576 nav = revnavgen(pos, pagelen, count, nodefunc)
577 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
577 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
578 entries=lambda **x: entries(limit=0, **x),
578 entries=lambda **x: entries(limit=0, **x),
579 latestentry=lambda **x: entries(limit=1, **x))
579 latestentry=lambda **x: entries(limit=1, **x))
580
580
581 def filerevision(self, tmpl, fctx):
581 def filerevision(self, tmpl, fctx):
582 f = fctx.path()
582 f = fctx.path()
583 text = fctx.data()
583 text = fctx.data()
584 fl = fctx.filelog()
584 fl = fctx.filelog()
585 n = fctx.filenode()
585 n = fctx.filenode()
586 parity = paritygen(self.stripecount)
586 parity = paritygen(self.stripecount)
587
587
588 mt = mimetypes.guess_type(f)[0]
588 mt = mimetypes.guess_type(f)[0]
589 rawtext = text
589 rawtext = text
590 if util.binary(text):
590 if util.binary(text):
591 mt = mt or 'application/octet-stream'
591 mt = mt or 'application/octet-stream'
592 text = "(binary:%s)" % mt
592 text = "(binary:%s)" % mt
593 mt = mt or 'text/plain'
593 mt = mt or 'text/plain'
594
594
595 def lines():
595 def lines():
596 for l, t in enumerate(text.splitlines(1)):
596 for l, t in enumerate(text.splitlines(1)):
597 yield {"line": t,
597 yield {"line": t,
598 "linenumber": "% 6d" % (l + 1),
598 "linenumber": "% 6d" % (l + 1),
599 "parity": parity.next()}
599 "parity": parity.next()}
600
600
601 return tmpl("filerevision",
601 return tmpl("filerevision",
602 file=f,
602 file=f,
603 path=_up(f),
603 path=_up(f),
604 text=lines(),
604 text=lines(),
605 raw=rawtext,
605 raw=rawtext,
606 mimetype=mt,
606 mimetype=mt,
607 rev=fctx.rev(),
607 rev=fctx.rev(),
608 node=hex(fctx.node()),
608 node=hex(fctx.node()),
609 author=fctx.user(),
609 author=fctx.user(),
610 date=fctx.date(),
610 date=fctx.date(),
611 desc=fctx.description(),
611 desc=fctx.description(),
612 parent=self.siblings(fctx.parents()),
612 parent=self.siblings(fctx.parents()),
613 child=self.siblings(fctx.children()),
613 child=self.siblings(fctx.children()),
614 rename=self.renamelink(fl, n),
614 rename=self.renamelink(fl, n),
615 permissions=fctx.manifest().flags(f))
615 permissions=fctx.manifest().flags(f))
616
616
617 def fileannotate(self, tmpl, fctx):
617 def fileannotate(self, tmpl, fctx):
618 f = fctx.path()
618 f = fctx.path()
619 n = fctx.filenode()
619 n = fctx.filenode()
620 fl = fctx.filelog()
620 fl = fctx.filelog()
621 parity = paritygen(self.stripecount)
621 parity = paritygen(self.stripecount)
622
622
623 def annotate(**map):
623 def annotate(**map):
624 last = None
624 last = None
625 for f, l in fctx.annotate(follow=True):
625 for f, l in fctx.annotate(follow=True):
626 fnode = f.filenode()
626 fnode = f.filenode()
627 name = self.repo.ui.shortuser(f.user())
627 name = self.repo.ui.shortuser(f.user())
628
628
629 if last != fnode:
629 if last != fnode:
630 last = fnode
630 last = fnode
631
631
632 yield {"parity": parity.next(),
632 yield {"parity": parity.next(),
633 "node": hex(f.node()),
633 "node": hex(f.node()),
634 "rev": f.rev(),
634 "rev": f.rev(),
635 "author": name,
635 "author": name,
636 "file": f.path(),
636 "file": f.path(),
637 "line": l}
637 "line": l}
638
638
639 return tmpl("fileannotate",
639 return tmpl("fileannotate",
640 file=f,
640 file=f,
641 annotate=annotate,
641 annotate=annotate,
642 path=_up(f),
642 path=_up(f),
643 rev=fctx.rev(),
643 rev=fctx.rev(),
644 node=hex(fctx.node()),
644 node=hex(fctx.node()),
645 author=fctx.user(),
645 author=fctx.user(),
646 date=fctx.date(),
646 date=fctx.date(),
647 desc=fctx.description(),
647 desc=fctx.description(),
648 rename=self.renamelink(fl, n),
648 rename=self.renamelink(fl, n),
649 parent=self.siblings(fctx.parents()),
649 parent=self.siblings(fctx.parents()),
650 child=self.siblings(fctx.children()),
650 child=self.siblings(fctx.children()),
651 permissions=fctx.manifest().flags(f))
651 permissions=fctx.manifest().flags(f))
652
652
653 def manifest(self, tmpl, ctx, path):
653 def manifest(self, tmpl, ctx, path):
654 mf = ctx.manifest()
654 mf = ctx.manifest()
655 node = ctx.node()
655 node = ctx.node()
656
656
657 files = {}
657 files = {}
658 parity = paritygen(self.stripecount)
658 parity = paritygen(self.stripecount)
659
659
660 if path and path[-1] != "/":
660 if path and path[-1] != "/":
661 path += "/"
661 path += "/"
662 l = len(path)
662 l = len(path)
663 abspath = "/" + path
663 abspath = "/" + path
664
664
665 for f, n in mf.items():
665 for f, n in mf.items():
666 if f[:l] != path:
666 if f[:l] != path:
667 continue
667 continue
668 remain = f[l:]
668 remain = f[l:]
669 if "/" in remain:
669 if "/" in remain:
670 short = remain[:remain.index("/") + 1] # bleah
670 short = remain[:remain.index("/") + 1] # bleah
671 files[short] = (f, None)
671 files[short] = (f, None)
672 else:
672 else:
673 short = os.path.basename(remain)
673 short = os.path.basename(remain)
674 files[short] = (f, n)
674 files[short] = (f, n)
675
675
676 if not files:
676 if not files:
677 raise ErrorResponse(404, 'Path not found: ' + path)
677 raise ErrorResponse(404, 'Path not found: ' + path)
678
678
679 def filelist(**map):
679 def filelist(**map):
680 fl = files.keys()
680 fl = files.keys()
681 fl.sort()
681 fl.sort()
682 for f in fl:
682 for f in fl:
683 full, fnode = files[f]
683 full, fnode = files[f]
684 if not fnode:
684 if not fnode:
685 continue
685 continue
686
686
687 fctx = ctx.filectx(full)
687 fctx = ctx.filectx(full)
688 yield {"file": full,
688 yield {"file": full,
689 "parity": parity.next(),
689 "parity": parity.next(),
690 "basename": f,
690 "basename": f,
691 "date": fctx.changectx().date(),
691 "date": fctx.changectx().date(),
692 "size": fctx.size(),
692 "size": fctx.size(),
693 "permissions": mf.flags(full)}
693 "permissions": mf.flags(full)}
694
694
695 def dirlist(**map):
695 def dirlist(**map):
696 fl = files.keys()
696 fl = files.keys()
697 fl.sort()
697 fl.sort()
698 for f in fl:
698 for f in fl:
699 full, fnode = files[f]
699 full, fnode = files[f]
700 if fnode:
700 if fnode:
701 continue
701 continue
702
702
703 yield {"parity": parity.next(),
703 yield {"parity": parity.next(),
704 "path": "%s%s" % (abspath, f),
704 "path": "%s%s" % (abspath, f),
705 "basename": f[:-1]}
705 "basename": f[:-1]}
706
706
707 return tmpl("manifest",
707 return tmpl("manifest",
708 rev=ctx.rev(),
708 rev=ctx.rev(),
709 node=hex(node),
709 node=hex(node),
710 path=abspath,
710 path=abspath,
711 up=_up(abspath),
711 up=_up(abspath),
712 upparity=parity.next(),
712 upparity=parity.next(),
713 fentries=filelist,
713 fentries=filelist,
714 dentries=dirlist,
714 dentries=dirlist,
715 archives=self.archivelist(hex(node)),
715 archives=self.archivelist(hex(node)),
716 tags=self.nodetagsdict(node),
716 tags=self.nodetagsdict(node),
717 branches=self.nodebranchdict(ctx))
717 branches=self.nodebranchdict(ctx))
718
718
719 def tags(self, tmpl):
719 def tags(self, tmpl):
720 i = self.repo.tagslist()
720 i = self.repo.tagslist()
721 i.reverse()
721 i.reverse()
722 parity = paritygen(self.stripecount)
722 parity = paritygen(self.stripecount)
723
723
724 def entries(notip=False,limit=0, **map):
724 def entries(notip=False,limit=0, **map):
725 count = 0
725 count = 0
726 for k, n in i:
726 for k, n in i:
727 if notip and k == "tip":
727 if notip and k == "tip":
728 continue
728 continue
729 if limit > 0 and count >= limit:
729 if limit > 0 and count >= limit:
730 continue
730 continue
731 count = count + 1
731 count = count + 1
732 yield {"parity": parity.next(),
732 yield {"parity": parity.next(),
733 "tag": k,
733 "tag": k,
734 "date": self.repo.changectx(n).date(),
734 "date": self.repo.changectx(n).date(),
735 "node": hex(n)}
735 "node": hex(n)}
736
736
737 return tmpl("tags",
737 return tmpl("tags",
738 node=hex(self.repo.changelog.tip()),
738 node=hex(self.repo.changelog.tip()),
739 entries=lambda **x: entries(False,0, **x),
739 entries=lambda **x: entries(False,0, **x),
740 entriesnotip=lambda **x: entries(True,0, **x),
740 entriesnotip=lambda **x: entries(True,0, **x),
741 latestentry=lambda **x: entries(True,1, **x))
741 latestentry=lambda **x: entries(True,1, **x))
742
742
743 def summary(self, tmpl):
743 def summary(self, tmpl):
744 i = self.repo.tagslist()
744 i = self.repo.tagslist()
745 i.reverse()
745 i.reverse()
746
746
747 def tagentries(**map):
747 def tagentries(**map):
748 parity = paritygen(self.stripecount)
748 parity = paritygen(self.stripecount)
749 count = 0
749 count = 0
750 for k, n in i:
750 for k, n in i:
751 if k == "tip": # skip tip
751 if k == "tip": # skip tip
752 continue;
752 continue;
753
753
754 count += 1
754 count += 1
755 if count > 10: # limit to 10 tags
755 if count > 10: # limit to 10 tags
756 break;
756 break;
757
757
758 yield tmpl("tagentry",
758 yield tmpl("tagentry",
759 parity=parity.next(),
759 parity=parity.next(),
760 tag=k,
760 tag=k,
761 node=hex(n),
761 node=hex(n),
762 date=self.repo.changectx(n).date())
762 date=self.repo.changectx(n).date())
763
763
764
764
765 def branches(**map):
765 def branches(**map):
766 parity = paritygen(self.stripecount)
766 parity = paritygen(self.stripecount)
767
767
768 b = self.repo.branchtags()
768 b = self.repo.branchtags()
769 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
769 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
770 l.sort()
770 l.sort()
771
771
772 for r,n,t in l:
772 for r,n,t in l:
773 ctx = self.repo.changectx(n)
773 ctx = self.repo.changectx(n)
774
774
775 yield {'parity': parity.next(),
775 yield {'parity': parity.next(),
776 'branch': t,
776 'branch': t,
777 'node': hex(n),
777 'node': hex(n),
778 'date': ctx.date()}
778 'date': ctx.date()}
779
779
780 def changelist(**map):
780 def changelist(**map):
781 parity = paritygen(self.stripecount, offset=start-end)
781 parity = paritygen(self.stripecount, offset=start-end)
782 l = [] # build a list in forward order for efficiency
782 l = [] # build a list in forward order for efficiency
783 for i in xrange(start, end):
783 for i in xrange(start, end):
784 ctx = self.repo.changectx(i)
784 ctx = self.repo.changectx(i)
785 n = ctx.node()
785 n = ctx.node()
786 hn = hex(n)
786 hn = hex(n)
787
787
788 l.insert(0, tmpl(
788 l.insert(0, tmpl(
789 'shortlogentry',
789 'shortlogentry',
790 parity=parity.next(),
790 parity=parity.next(),
791 author=ctx.user(),
791 author=ctx.user(),
792 desc=ctx.description(),
792 desc=ctx.description(),
793 date=ctx.date(),
793 date=ctx.date(),
794 rev=i,
794 rev=i,
795 node=hn,
795 node=hn,
796 tags=self.nodetagsdict(n),
796 tags=self.nodetagsdict(n),
797 branches=self.nodebranchdict(ctx)))
797 branches=self.nodebranchdict(ctx)))
798
798
799 yield l
799 yield l
800
800
801 cl = self.repo.changelog
801 cl = self.repo.changelog
802 count = cl.count()
802 count = cl.count()
803 start = max(0, count - self.maxchanges)
803 start = max(0, count - self.maxchanges)
804 end = min(count, start + self.maxchanges)
804 end = min(count, start + self.maxchanges)
805
805
806 return tmpl("summary",
806 return tmpl("summary",
807 desc=self.config("web", "description", "unknown"),
807 desc=self.config("web", "description", "unknown"),
808 owner=get_contact(self.config) or "unknown",
808 owner=get_contact(self.config) or "unknown",
809 lastchange=cl.read(cl.tip())[2],
809 lastchange=cl.read(cl.tip())[2],
810 tags=tagentries,
810 tags=tagentries,
811 branches=branches,
811 branches=branches,
812 shortlog=changelist,
812 shortlog=changelist,
813 node=hex(cl.tip()),
813 node=hex(cl.tip()),
814 archives=self.archivelist("tip"))
814 archives=self.archivelist("tip"))
815
815
816 def filediff(self, tmpl, fctx):
816 def filediff(self, tmpl, fctx):
817 n = fctx.node()
817 n = fctx.node()
818 path = fctx.path()
818 path = fctx.path()
819 parents = fctx.parents()
819 parents = fctx.parents()
820 p1 = parents and parents[0].node() or nullid
820 p1 = parents and parents[0].node() or nullid
821
821
822 def diff(**map):
822 def diff(**map):
823 yield self.diff(tmpl, p1, n, [path])
823 yield self.diff(tmpl, p1, n, [path])
824
824
825 return tmpl("filediff",
825 return tmpl("filediff",
826 file=path,
826 file=path,
827 node=hex(n),
827 node=hex(n),
828 rev=fctx.rev(),
828 rev=fctx.rev(),
829 parent=self.siblings(parents),
829 parent=self.siblings(parents),
830 child=self.siblings(fctx.children()),
830 child=self.siblings(fctx.children()),
831 diff=diff)
831 diff=diff)
832
832
833 archive_specs = {
833 archive_specs = {
834 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
834 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
835 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
835 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
836 'zip': ('application/zip', 'zip', '.zip', None),
836 'zip': ('application/zip', 'zip', '.zip', None),
837 }
837 }
838
838
839 def archive(self, tmpl, req, key, type_):
839 def archive(self, tmpl, req, key, type_):
840 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
840 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
841 cnode = self.repo.lookup(key)
841 cnode = self.repo.lookup(key)
842 arch_version = key
842 arch_version = key
843 if cnode == key or key == 'tip':
843 if cnode == key or key == 'tip':
844 arch_version = short(cnode)
844 arch_version = short(cnode)
845 name = "%s-%s" % (reponame, arch_version)
845 name = "%s-%s" % (reponame, arch_version)
846 mimetype, artype, extension, encoding = self.archive_specs[type_]
846 mimetype, artype, extension, encoding = self.archive_specs[type_]
847 headers = [('Content-type', mimetype),
847 headers = [
848 ('Content-disposition', 'attachment; filename=%s%s' %
848 ('Content-Type', mimetype),
849 (name, extension))]
849 ('Content-Disposition', 'attachment; filename=%s%s' %
850 (name, extension))
851 ]
850 if encoding:
852 if encoding:
851 headers.append(('Content-encoding', encoding))
853 headers.append(('Content-Encoding', encoding))
852 req.header(headers)
854 req.header(headers)
853 archival.archive(self.repo, req, cnode, artype, prefix=name)
855 archival.archive(self.repo, req, cnode, artype, prefix=name)
854
856
855 # add tags to things
857 # add tags to things
856 # tags -> list of changesets corresponding to tags
858 # tags -> list of changesets corresponding to tags
857 # find tag, changeset, file
859 # find tag, changeset, file
858
860
859 def cleanpath(self, path):
861 def cleanpath(self, path):
860 path = path.lstrip('/')
862 path = path.lstrip('/')
861 return util.canonpath(self.repo.root, '', path)
863 return util.canonpath(self.repo.root, '', path)
862
864
863 def changectx(self, req):
865 def changectx(self, req):
864 if 'node' in req.form:
866 if 'node' in req.form:
865 changeid = req.form['node'][0]
867 changeid = req.form['node'][0]
866 elif 'manifest' in req.form:
868 elif 'manifest' in req.form:
867 changeid = req.form['manifest'][0]
869 changeid = req.form['manifest'][0]
868 else:
870 else:
869 changeid = self.repo.changelog.count() - 1
871 changeid = self.repo.changelog.count() - 1
870
872
871 try:
873 try:
872 ctx = self.repo.changectx(changeid)
874 ctx = self.repo.changectx(changeid)
873 except hg.RepoError:
875 except hg.RepoError:
874 man = self.repo.manifest
876 man = self.repo.manifest
875 mn = man.lookup(changeid)
877 mn = man.lookup(changeid)
876 ctx = self.repo.changectx(man.linkrev(mn))
878 ctx = self.repo.changectx(man.linkrev(mn))
877
879
878 return ctx
880 return ctx
879
881
880 def filectx(self, req):
882 def filectx(self, req):
881 path = self.cleanpath(req.form['file'][0])
883 path = self.cleanpath(req.form['file'][0])
882 if 'node' in req.form:
884 if 'node' in req.form:
883 changeid = req.form['node'][0]
885 changeid = req.form['node'][0]
884 else:
886 else:
885 changeid = req.form['filenode'][0]
887 changeid = req.form['filenode'][0]
886 try:
888 try:
887 ctx = self.repo.changectx(changeid)
889 ctx = self.repo.changectx(changeid)
888 fctx = ctx.filectx(path)
890 fctx = ctx.filectx(path)
889 except hg.RepoError:
891 except hg.RepoError:
890 fctx = self.repo.filectx(path, fileid=changeid)
892 fctx = self.repo.filectx(path, fileid=changeid)
891
893
892 return fctx
894 return fctx
893
895
894 def check_perm(self, req, op, default):
896 def check_perm(self, req, op, default):
895 '''check permission for operation based on user auth.
897 '''check permission for operation based on user auth.
896 return true if op allowed, else false.
898 return true if op allowed, else false.
897 default is policy to use if no config given.'''
899 default is policy to use if no config given.'''
898
900
899 user = req.env.get('REMOTE_USER')
901 user = req.env.get('REMOTE_USER')
900
902
901 deny = self.configlist('web', 'deny_' + op)
903 deny = self.configlist('web', 'deny_' + op)
902 if deny and (not user or deny == ['*'] or user in deny):
904 if deny and (not user or deny == ['*'] or user in deny):
903 return False
905 return False
904
906
905 allow = self.configlist('web', 'allow_' + op)
907 allow = self.configlist('web', 'allow_' + op)
906 return (allow and (allow == ['*'] or user in allow)) or default
908 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,102 +1,102
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import socket, cgi, errno
9 import socket, cgi, errno
10 from mercurial.i18n import gettext as _
10 from mercurial.i18n import gettext as _
11 from common import ErrorResponse, statusmessage
11 from common import ErrorResponse, statusmessage
12
12
13 class wsgirequest(object):
13 class wsgirequest(object):
14 def __init__(self, wsgienv, start_response):
14 def __init__(self, wsgienv, start_response):
15 version = wsgienv['wsgi.version']
15 version = wsgienv['wsgi.version']
16 if (version < (1, 0)) or (version >= (2, 0)):
16 if (version < (1, 0)) or (version >= (2, 0)):
17 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
17 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
18 % version)
18 % version)
19 self.inp = wsgienv['wsgi.input']
19 self.inp = wsgienv['wsgi.input']
20 self.server_write = None
20 self.server_write = None
21 self.err = wsgienv['wsgi.errors']
21 self.err = wsgienv['wsgi.errors']
22 self.threaded = wsgienv['wsgi.multithread']
22 self.threaded = wsgienv['wsgi.multithread']
23 self.multiprocess = wsgienv['wsgi.multiprocess']
23 self.multiprocess = wsgienv['wsgi.multiprocess']
24 self.run_once = wsgienv['wsgi.run_once']
24 self.run_once = wsgienv['wsgi.run_once']
25 self.env = wsgienv
25 self.env = wsgienv
26 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
26 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
27 self._start_response = start_response
27 self._start_response = start_response
28 self.headers = []
28 self.headers = []
29
29
30 def __iter__(self):
30 def __iter__(self):
31 return iter([])
31 return iter([])
32
32
33 def read(self, count=-1):
33 def read(self, count=-1):
34 return self.inp.read(count)
34 return self.inp.read(count)
35
35
36 def start_response(self, status):
36 def start_response(self, status):
37 if self._start_response is not None:
37 if self._start_response is not None:
38 if not self.headers:
38 if not self.headers:
39 raise RuntimeError("request.write called before headers sent")
39 raise RuntimeError("request.write called before headers sent")
40
40
41 for k, v in self.headers:
41 for k, v in self.headers:
42 if not isinstance(v, str):
42 if not isinstance(v, str):
43 raise TypeError('header value must be string: %r' % v)
43 raise TypeError('header value must be string: %r' % v)
44
44
45 if isinstance(status, ErrorResponse):
45 if isinstance(status, ErrorResponse):
46 status = statusmessage(status.code)
46 status = statusmessage(status.code)
47 elif isinstance(status, int):
47 elif isinstance(status, int):
48 status = statusmessage(status)
48 status = statusmessage(status)
49
49
50 self.server_write = self._start_response(status, self.headers)
50 self.server_write = self._start_response(status, self.headers)
51 self._start_response = None
51 self._start_response = None
52 self.headers = []
52 self.headers = []
53
53
54 def respond(self, status, *things):
54 def respond(self, status, *things):
55 if not things:
55 if not things:
56 self.start_response(status)
56 self.start_response(status)
57 for thing in things:
57 for thing in things:
58 if hasattr(thing, "__iter__"):
58 if hasattr(thing, "__iter__"):
59 for part in thing:
59 for part in thing:
60 self.respond(status, part)
60 self.respond(status, part)
61 else:
61 else:
62 thing = str(thing)
62 thing = str(thing)
63 self.start_response(status)
63 self.start_response(status)
64 try:
64 try:
65 self.server_write(thing)
65 self.server_write(thing)
66 except socket.error, inst:
66 except socket.error, inst:
67 if inst[0] != errno.ECONNRESET:
67 if inst[0] != errno.ECONNRESET:
68 raise
68 raise
69
69
70 def write(self, *things):
70 def write(self, *things):
71 self.respond('200 Script output follows', *things)
71 self.respond('200 Script output follows', *things)
72
72
73 def writelines(self, lines):
73 def writelines(self, lines):
74 for line in lines:
74 for line in lines:
75 self.write(line)
75 self.write(line)
76
76
77 def flush(self):
77 def flush(self):
78 return None
78 return None
79
79
80 def close(self):
80 def close(self):
81 return None
81 return None
82
82
83 def header(self, headers=[('Content-type','text/html')]):
83 def header(self, headers=[('Content-Type','text/html')]):
84 self.headers.extend(headers)
84 self.headers.extend(headers)
85
85
86 def httphdr(self, type, filename=None, length=0, headers={}):
86 def httphdr(self, type, filename=None, length=0, headers={}):
87 headers = headers.items()
87 headers = headers.items()
88 headers.append(('Content-type', type))
88 headers.append(('Content-Type', type))
89 if filename:
89 if filename:
90 headers.append(('Content-disposition', 'inline; filename=%s' %
90 headers.append(('Content-Disposition', 'inline; filename=%s' %
91 filename))
91 filename))
92 if length:
92 if length:
93 headers.append(('Content-length', str(length)))
93 headers.append(('Content-Length', str(length)))
94 self.header(headers)
94 self.header(headers)
95
95
96 def wsgiapplication(app_maker):
96 def wsgiapplication(app_maker):
97 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
97 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
98 can and should now be used as a WSGI application.'''
98 can and should now be used as a WSGI application.'''
99 application = app_maker()
99 application = app_maker()
100 def run_wsgi(env, respond):
100 def run_wsgi(env, respond):
101 return application(env, respond)
101 return application(env, respond)
102 return run_wsgi
102 return run_wsgi
@@ -1,462 +1,462
1 # httprepo.py - HTTP repository proxy classes for mercurial
1 # httprepo.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
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from node import *
9 from node import *
10 from remoterepo import *
10 from remoterepo import *
11 from i18n import _
11 from i18n import _
12 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
12 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
13 import errno, keepalive, tempfile, socket, changegroup
13 import errno, keepalive, tempfile, socket, changegroup
14
14
15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
15 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
16 def __init__(self, ui):
16 def __init__(self, ui):
17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
17 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
18 self.ui = ui
18 self.ui = ui
19
19
20 def find_user_password(self, realm, authuri):
20 def find_user_password(self, realm, authuri):
21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
21 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
22 self, realm, authuri)
22 self, realm, authuri)
23 user, passwd = authinfo
23 user, passwd = authinfo
24 if user and passwd:
24 if user and passwd:
25 return (user, passwd)
25 return (user, passwd)
26
26
27 if not self.ui.interactive:
27 if not self.ui.interactive:
28 raise util.Abort(_('http authorization required'))
28 raise util.Abort(_('http authorization required'))
29
29
30 self.ui.write(_("http authorization required\n"))
30 self.ui.write(_("http authorization required\n"))
31 self.ui.status(_("realm: %s\n") % realm)
31 self.ui.status(_("realm: %s\n") % realm)
32 if user:
32 if user:
33 self.ui.status(_("user: %s\n") % user)
33 self.ui.status(_("user: %s\n") % user)
34 else:
34 else:
35 user = self.ui.prompt(_("user:"), default=None)
35 user = self.ui.prompt(_("user:"), default=None)
36
36
37 if not passwd:
37 if not passwd:
38 passwd = self.ui.getpass()
38 passwd = self.ui.getpass()
39
39
40 self.add_password(realm, authuri, user, passwd)
40 self.add_password(realm, authuri, user, passwd)
41 return (user, passwd)
41 return (user, passwd)
42
42
43 def netlocsplit(netloc):
43 def netlocsplit(netloc):
44 '''split [user[:passwd]@]host[:port] into 4-tuple.'''
44 '''split [user[:passwd]@]host[:port] into 4-tuple.'''
45
45
46 a = netloc.find('@')
46 a = netloc.find('@')
47 if a == -1:
47 if a == -1:
48 user, passwd = None, None
48 user, passwd = None, None
49 else:
49 else:
50 userpass, netloc = netloc[:a], netloc[a+1:]
50 userpass, netloc = netloc[:a], netloc[a+1:]
51 c = userpass.find(':')
51 c = userpass.find(':')
52 if c == -1:
52 if c == -1:
53 user, passwd = urllib.unquote(userpass), None
53 user, passwd = urllib.unquote(userpass), None
54 else:
54 else:
55 user = urllib.unquote(userpass[:c])
55 user = urllib.unquote(userpass[:c])
56 passwd = urllib.unquote(userpass[c+1:])
56 passwd = urllib.unquote(userpass[c+1:])
57 c = netloc.find(':')
57 c = netloc.find(':')
58 if c == -1:
58 if c == -1:
59 host, port = netloc, None
59 host, port = netloc, None
60 else:
60 else:
61 host, port = netloc[:c], netloc[c+1:]
61 host, port = netloc[:c], netloc[c+1:]
62 return host, port, user, passwd
62 return host, port, user, passwd
63
63
64 def netlocunsplit(host, port, user=None, passwd=None):
64 def netlocunsplit(host, port, user=None, passwd=None):
65 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
65 '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
66 if port:
66 if port:
67 hostport = host + ':' + port
67 hostport = host + ':' + port
68 else:
68 else:
69 hostport = host
69 hostport = host
70 if user:
70 if user:
71 if passwd:
71 if passwd:
72 userpass = urllib.quote(user) + ':' + urllib.quote(passwd)
72 userpass = urllib.quote(user) + ':' + urllib.quote(passwd)
73 else:
73 else:
74 userpass = urllib.quote(user)
74 userpass = urllib.quote(user)
75 return userpass + '@' + hostport
75 return userpass + '@' + hostport
76 return hostport
76 return hostport
77
77
78 # work around a bug in Python < 2.4.2
78 # work around a bug in Python < 2.4.2
79 # (it leaves a "\n" at the end of Proxy-authorization headers)
79 # (it leaves a "\n" at the end of Proxy-authorization headers)
80 class request(urllib2.Request):
80 class request(urllib2.Request):
81 def add_header(self, key, val):
81 def add_header(self, key, val):
82 if key.lower() == 'proxy-authorization':
82 if key.lower() == 'proxy-authorization':
83 val = val.strip()
83 val = val.strip()
84 return urllib2.Request.add_header(self, key, val)
84 return urllib2.Request.add_header(self, key, val)
85
85
86 class httpsendfile(file):
86 class httpsendfile(file):
87 def __len__(self):
87 def __len__(self):
88 return os.fstat(self.fileno()).st_size
88 return os.fstat(self.fileno()).st_size
89
89
90 def _gen_sendfile(connection):
90 def _gen_sendfile(connection):
91 def _sendfile(self, data):
91 def _sendfile(self, data):
92 # send a file
92 # send a file
93 if isinstance(data, httpsendfile):
93 if isinstance(data, httpsendfile):
94 # if auth required, some data sent twice, so rewind here
94 # if auth required, some data sent twice, so rewind here
95 data.seek(0)
95 data.seek(0)
96 for chunk in util.filechunkiter(data):
96 for chunk in util.filechunkiter(data):
97 connection.send(self, chunk)
97 connection.send(self, chunk)
98 else:
98 else:
99 connection.send(self, data)
99 connection.send(self, data)
100 return _sendfile
100 return _sendfile
101
101
102 class httpconnection(keepalive.HTTPConnection):
102 class httpconnection(keepalive.HTTPConnection):
103 # must be able to send big bundle as stream.
103 # must be able to send big bundle as stream.
104 send = _gen_sendfile(keepalive.HTTPConnection)
104 send = _gen_sendfile(keepalive.HTTPConnection)
105
105
106 class basehttphandler(keepalive.HTTPHandler):
106 class basehttphandler(keepalive.HTTPHandler):
107 def http_open(self, req):
107 def http_open(self, req):
108 return self.do_open(httpconnection, req)
108 return self.do_open(httpconnection, req)
109
109
110 has_https = hasattr(urllib2, 'HTTPSHandler')
110 has_https = hasattr(urllib2, 'HTTPSHandler')
111 if has_https:
111 if has_https:
112 class httpsconnection(httplib.HTTPSConnection):
112 class httpsconnection(httplib.HTTPSConnection):
113 response_class = keepalive.HTTPResponse
113 response_class = keepalive.HTTPResponse
114 # must be able to send big bundle as stream.
114 # must be able to send big bundle as stream.
115 send = _gen_sendfile(httplib.HTTPSConnection)
115 send = _gen_sendfile(httplib.HTTPSConnection)
116
116
117 class httphandler(basehttphandler, urllib2.HTTPSHandler):
117 class httphandler(basehttphandler, urllib2.HTTPSHandler):
118 def https_open(self, req):
118 def https_open(self, req):
119 return self.do_open(httpsconnection, req)
119 return self.do_open(httpsconnection, req)
120 else:
120 else:
121 class httphandler(basehttphandler):
121 class httphandler(basehttphandler):
122 pass
122 pass
123
123
124 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
124 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
125 # it doesn't know about the auth type requested. This can happen if
125 # it doesn't know about the auth type requested. This can happen if
126 # somebody is using BasicAuth and types a bad password.
126 # somebody is using BasicAuth and types a bad password.
127 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
127 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
128 def http_error_auth_reqed(self, auth_header, host, req, headers):
128 def http_error_auth_reqed(self, auth_header, host, req, headers):
129 try:
129 try:
130 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
130 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
131 self, auth_header, host, req, headers)
131 self, auth_header, host, req, headers)
132 except ValueError, inst:
132 except ValueError, inst:
133 arg = inst.args[0]
133 arg = inst.args[0]
134 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
134 if arg.startswith("AbstractDigestAuthHandler doesn't know "):
135 return
135 return
136 raise
136 raise
137
137
138 def zgenerator(f):
138 def zgenerator(f):
139 zd = zlib.decompressobj()
139 zd = zlib.decompressobj()
140 try:
140 try:
141 for chunk in util.filechunkiter(f):
141 for chunk in util.filechunkiter(f):
142 yield zd.decompress(chunk)
142 yield zd.decompress(chunk)
143 except httplib.HTTPException, inst:
143 except httplib.HTTPException, inst:
144 raise IOError(None, _('connection ended unexpectedly'))
144 raise IOError(None, _('connection ended unexpectedly'))
145 yield zd.flush()
145 yield zd.flush()
146
146
147 _safe = ('abcdefghijklmnopqrstuvwxyz'
147 _safe = ('abcdefghijklmnopqrstuvwxyz'
148 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
148 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
149 '0123456789' '_.-/')
149 '0123456789' '_.-/')
150 _safeset = None
150 _safeset = None
151 _hex = None
151 _hex = None
152 def quotepath(path):
152 def quotepath(path):
153 '''quote the path part of a URL
153 '''quote the path part of a URL
154
154
155 This is similar to urllib.quote, but it also tries to avoid
155 This is similar to urllib.quote, but it also tries to avoid
156 quoting things twice (inspired by wget):
156 quoting things twice (inspired by wget):
157
157
158 >>> quotepath('abc def')
158 >>> quotepath('abc def')
159 'abc%20def'
159 'abc%20def'
160 >>> quotepath('abc%20def')
160 >>> quotepath('abc%20def')
161 'abc%20def'
161 'abc%20def'
162 >>> quotepath('abc%20 def')
162 >>> quotepath('abc%20 def')
163 'abc%20%20def'
163 'abc%20%20def'
164 >>> quotepath('abc def%20')
164 >>> quotepath('abc def%20')
165 'abc%20def%20'
165 'abc%20def%20'
166 >>> quotepath('abc def%2')
166 >>> quotepath('abc def%2')
167 'abc%20def%252'
167 'abc%20def%252'
168 >>> quotepath('abc def%')
168 >>> quotepath('abc def%')
169 'abc%20def%25'
169 'abc%20def%25'
170 '''
170 '''
171 global _safeset, _hex
171 global _safeset, _hex
172 if _safeset is None:
172 if _safeset is None:
173 _safeset = util.set(_safe)
173 _safeset = util.set(_safe)
174 _hex = util.set('abcdefABCDEF0123456789')
174 _hex = util.set('abcdefABCDEF0123456789')
175 l = list(path)
175 l = list(path)
176 for i in xrange(len(l)):
176 for i in xrange(len(l)):
177 c = l[i]
177 c = l[i]
178 if c == '%' and i + 2 < len(l) and (l[i+1] in _hex and l[i+2] in _hex):
178 if c == '%' and i + 2 < len(l) and (l[i+1] in _hex and l[i+2] in _hex):
179 pass
179 pass
180 elif c not in _safeset:
180 elif c not in _safeset:
181 l[i] = '%%%02X' % ord(c)
181 l[i] = '%%%02X' % ord(c)
182 return ''.join(l)
182 return ''.join(l)
183
183
184 class httprepository(remoterepository):
184 class httprepository(remoterepository):
185 def __init__(self, ui, path):
185 def __init__(self, ui, path):
186 self.path = path
186 self.path = path
187 self.caps = None
187 self.caps = None
188 self.handler = None
188 self.handler = None
189 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
189 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
190 if query or frag:
190 if query or frag:
191 raise util.Abort(_('unsupported URL component: "%s"') %
191 raise util.Abort(_('unsupported URL component: "%s"') %
192 (query or frag))
192 (query or frag))
193 if not urlpath:
193 if not urlpath:
194 urlpath = '/'
194 urlpath = '/'
195 urlpath = quotepath(urlpath)
195 urlpath = quotepath(urlpath)
196 host, port, user, passwd = netlocsplit(netloc)
196 host, port, user, passwd = netlocsplit(netloc)
197
197
198 # urllib cannot handle URLs with embedded user or passwd
198 # urllib cannot handle URLs with embedded user or passwd
199 self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
199 self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
200 urlpath, '', ''))
200 urlpath, '', ''))
201 self.ui = ui
201 self.ui = ui
202 self.ui.debug(_('using %s\n') % self._url)
202 self.ui.debug(_('using %s\n') % self._url)
203
203
204 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
204 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
205 # XXX proxyauthinfo = None
205 # XXX proxyauthinfo = None
206 self.handler = httphandler()
206 self.handler = httphandler()
207 handlers = [self.handler]
207 handlers = [self.handler]
208
208
209 if proxyurl:
209 if proxyurl:
210 # proxy can be proper url or host[:port]
210 # proxy can be proper url or host[:port]
211 if not (proxyurl.startswith('http:') or
211 if not (proxyurl.startswith('http:') or
212 proxyurl.startswith('https:')):
212 proxyurl.startswith('https:')):
213 proxyurl = 'http://' + proxyurl + '/'
213 proxyurl = 'http://' + proxyurl + '/'
214 snpqf = urlparse.urlsplit(proxyurl)
214 snpqf = urlparse.urlsplit(proxyurl)
215 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
215 proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
216 hpup = netlocsplit(proxynetloc)
216 hpup = netlocsplit(proxynetloc)
217
217
218 proxyhost, proxyport, proxyuser, proxypasswd = hpup
218 proxyhost, proxyport, proxyuser, proxypasswd = hpup
219 if not proxyuser:
219 if not proxyuser:
220 proxyuser = ui.config("http_proxy", "user")
220 proxyuser = ui.config("http_proxy", "user")
221 proxypasswd = ui.config("http_proxy", "passwd")
221 proxypasswd = ui.config("http_proxy", "passwd")
222
222
223 # see if we should use a proxy for this url
223 # see if we should use a proxy for this url
224 no_list = [ "localhost", "127.0.0.1" ]
224 no_list = [ "localhost", "127.0.0.1" ]
225 no_list.extend([p.lower() for
225 no_list.extend([p.lower() for
226 p in ui.configlist("http_proxy", "no")])
226 p in ui.configlist("http_proxy", "no")])
227 no_list.extend([p.strip().lower() for
227 no_list.extend([p.strip().lower() for
228 p in os.getenv("no_proxy", '').split(',')
228 p in os.getenv("no_proxy", '').split(',')
229 if p.strip()])
229 if p.strip()])
230 # "http_proxy.always" config is for running tests on localhost
230 # "http_proxy.always" config is for running tests on localhost
231 if (not ui.configbool("http_proxy", "always") and
231 if (not ui.configbool("http_proxy", "always") and
232 host.lower() in no_list):
232 host.lower() in no_list):
233 # avoid auto-detection of proxy settings by appending
233 # avoid auto-detection of proxy settings by appending
234 # a ProxyHandler with no proxies defined.
234 # a ProxyHandler with no proxies defined.
235 handlers.append(urllib2.ProxyHandler({}))
235 handlers.append(urllib2.ProxyHandler({}))
236 ui.debug(_('disabling proxy for %s\n') % host)
236 ui.debug(_('disabling proxy for %s\n') % host)
237 else:
237 else:
238 proxyurl = urlparse.urlunsplit((
238 proxyurl = urlparse.urlunsplit((
239 proxyscheme, netlocunsplit(proxyhost, proxyport,
239 proxyscheme, netlocunsplit(proxyhost, proxyport,
240 proxyuser, proxypasswd or ''),
240 proxyuser, proxypasswd or ''),
241 proxypath, proxyquery, proxyfrag))
241 proxypath, proxyquery, proxyfrag))
242 handlers.append(urllib2.ProxyHandler({scheme: proxyurl}))
242 handlers.append(urllib2.ProxyHandler({scheme: proxyurl}))
243 ui.debug(_('proxying through http://%s:%s\n') %
243 ui.debug(_('proxying through http://%s:%s\n') %
244 (proxyhost, proxyport))
244 (proxyhost, proxyport))
245
245
246 # urllib2 takes proxy values from the environment and those
246 # urllib2 takes proxy values from the environment and those
247 # will take precedence if found, so drop them
247 # will take precedence if found, so drop them
248 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
248 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
249 try:
249 try:
250 if env in os.environ:
250 if env in os.environ:
251 del os.environ[env]
251 del os.environ[env]
252 except OSError:
252 except OSError:
253 pass
253 pass
254
254
255 passmgr = passwordmgr(ui)
255 passmgr = passwordmgr(ui)
256 if user:
256 if user:
257 ui.debug(_('http auth: user %s, password %s\n') %
257 ui.debug(_('http auth: user %s, password %s\n') %
258 (user, passwd and '*' * len(passwd) or 'not set'))
258 (user, passwd and '*' * len(passwd) or 'not set'))
259 netloc = host
259 netloc = host
260 if port:
260 if port:
261 netloc += ':' + port
261 netloc += ':' + port
262 # Python < 2.4.3 uses only the netloc to search for a password
262 # Python < 2.4.3 uses only the netloc to search for a password
263 passmgr.add_password(None, (self._url, netloc), user, passwd or '')
263 passmgr.add_password(None, (self._url, netloc), user, passwd or '')
264
264
265 handlers.extend((urllib2.HTTPBasicAuthHandler(passmgr),
265 handlers.extend((urllib2.HTTPBasicAuthHandler(passmgr),
266 httpdigestauthhandler(passmgr)))
266 httpdigestauthhandler(passmgr)))
267 opener = urllib2.build_opener(*handlers)
267 opener = urllib2.build_opener(*handlers)
268
268
269 # 1.0 here is the _protocol_ version
269 # 1.0 here is the _protocol_ version
270 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
270 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
271 urllib2.install_opener(opener)
271 urllib2.install_opener(opener)
272
272
273 def __del__(self):
273 def __del__(self):
274 if self.handler:
274 if self.handler:
275 self.handler.close_all()
275 self.handler.close_all()
276 self.handler = None
276 self.handler = None
277
277
278 def url(self):
278 def url(self):
279 return self.path
279 return self.path
280
280
281 # look up capabilities only when needed
281 # look up capabilities only when needed
282
282
283 def get_caps(self):
283 def get_caps(self):
284 if self.caps is None:
284 if self.caps is None:
285 try:
285 try:
286 self.caps = util.set(self.do_read('capabilities').split())
286 self.caps = util.set(self.do_read('capabilities').split())
287 except repo.RepoError:
287 except repo.RepoError:
288 self.caps = util.set()
288 self.caps = util.set()
289 self.ui.debug(_('capabilities: %s\n') %
289 self.ui.debug(_('capabilities: %s\n') %
290 (' '.join(self.caps or ['none'])))
290 (' '.join(self.caps or ['none'])))
291 return self.caps
291 return self.caps
292
292
293 capabilities = property(get_caps)
293 capabilities = property(get_caps)
294
294
295 def lock(self):
295 def lock(self):
296 raise util.Abort(_('operation not supported over http'))
296 raise util.Abort(_('operation not supported over http'))
297
297
298 def do_cmd(self, cmd, **args):
298 def do_cmd(self, cmd, **args):
299 data = args.pop('data', None)
299 data = args.pop('data', None)
300 headers = args.pop('headers', {})
300 headers = args.pop('headers', {})
301 self.ui.debug(_("sending %s command\n") % cmd)
301 self.ui.debug(_("sending %s command\n") % cmd)
302 q = {"cmd": cmd}
302 q = {"cmd": cmd}
303 q.update(args)
303 q.update(args)
304 qs = '?%s' % urllib.urlencode(q)
304 qs = '?%s' % urllib.urlencode(q)
305 cu = "%s%s" % (self._url, qs)
305 cu = "%s%s" % (self._url, qs)
306 try:
306 try:
307 if data:
307 if data:
308 self.ui.debug(_("sending %s bytes\n") % len(data))
308 self.ui.debug(_("sending %s bytes\n") % len(data))
309 resp = urllib2.urlopen(request(cu, data, headers))
309 resp = urllib2.urlopen(request(cu, data, headers))
310 except urllib2.HTTPError, inst:
310 except urllib2.HTTPError, inst:
311 if inst.code == 401:
311 if inst.code == 401:
312 raise util.Abort(_('authorization failed'))
312 raise util.Abort(_('authorization failed'))
313 raise
313 raise
314 except httplib.HTTPException, inst:
314 except httplib.HTTPException, inst:
315 self.ui.debug(_('http error while sending %s command\n') % cmd)
315 self.ui.debug(_('http error while sending %s command\n') % cmd)
316 self.ui.print_exc()
316 self.ui.print_exc()
317 raise IOError(None, inst)
317 raise IOError(None, inst)
318 except IndexError:
318 except IndexError:
319 # this only happens with Python 2.3, later versions raise URLError
319 # this only happens with Python 2.3, later versions raise URLError
320 raise util.Abort(_('http error, possibly caused by proxy setting'))
320 raise util.Abort(_('http error, possibly caused by proxy setting'))
321 # record the url we got redirected to
321 # record the url we got redirected to
322 resp_url = resp.geturl()
322 resp_url = resp.geturl()
323 if resp_url.endswith(qs):
323 if resp_url.endswith(qs):
324 resp_url = resp_url[:-len(qs)]
324 resp_url = resp_url[:-len(qs)]
325 if self._url != resp_url:
325 if self._url != resp_url:
326 self.ui.status(_('real URL is %s\n') % resp_url)
326 self.ui.status(_('real URL is %s\n') % resp_url)
327 self._url = resp_url
327 self._url = resp_url
328 try:
328 try:
329 proto = resp.getheader('content-type')
329 proto = resp.getheader('content-type')
330 except AttributeError:
330 except AttributeError:
331 proto = resp.headers['content-type']
331 proto = resp.headers['content-type']
332
332
333 # accept old "text/plain" and "application/hg-changegroup" for now
333 # accept old "text/plain" and "application/hg-changegroup" for now
334 if not (proto.startswith('application/mercurial-') or
334 if not (proto.startswith('application/mercurial-') or
335 proto.startswith('text/plain') or
335 proto.startswith('text/plain') or
336 proto.startswith('application/hg-changegroup')):
336 proto.startswith('application/hg-changegroup')):
337 self.ui.debug(_("Requested URL: '%s'\n") % cu)
337 self.ui.debug(_("Requested URL: '%s'\n") % cu)
338 raise repo.RepoError(_("'%s' does not appear to be an hg repository")
338 raise repo.RepoError(_("'%s' does not appear to be an hg repository")
339 % self._url)
339 % self._url)
340
340
341 if proto.startswith('application/mercurial-'):
341 if proto.startswith('application/mercurial-'):
342 try:
342 try:
343 version = proto.split('-', 1)[1]
343 version = proto.split('-', 1)[1]
344 version_info = tuple([int(n) for n in version.split('.')])
344 version_info = tuple([int(n) for n in version.split('.')])
345 except ValueError:
345 except ValueError:
346 raise repo.RepoError(_("'%s' sent a broken Content-type "
346 raise repo.RepoError(_("'%s' sent a broken Content-Type "
347 "header (%s)") % (self._url, proto))
347 "header (%s)") % (self._url, proto))
348 if version_info > (0, 1):
348 if version_info > (0, 1):
349 raise repo.RepoError(_("'%s' uses newer protocol %s") %
349 raise repo.RepoError(_("'%s' uses newer protocol %s") %
350 (self._url, version))
350 (self._url, version))
351
351
352 return resp
352 return resp
353
353
354 def do_read(self, cmd, **args):
354 def do_read(self, cmd, **args):
355 fp = self.do_cmd(cmd, **args)
355 fp = self.do_cmd(cmd, **args)
356 try:
356 try:
357 return fp.read()
357 return fp.read()
358 finally:
358 finally:
359 # if using keepalive, allow connection to be reused
359 # if using keepalive, allow connection to be reused
360 fp.close()
360 fp.close()
361
361
362 def lookup(self, key):
362 def lookup(self, key):
363 self.requirecap('lookup', _('look up remote revision'))
363 self.requirecap('lookup', _('look up remote revision'))
364 d = self.do_cmd("lookup", key = key).read()
364 d = self.do_cmd("lookup", key = key).read()
365 success, data = d[:-1].split(' ', 1)
365 success, data = d[:-1].split(' ', 1)
366 if int(success):
366 if int(success):
367 return bin(data)
367 return bin(data)
368 raise repo.RepoError(data)
368 raise repo.RepoError(data)
369
369
370 def heads(self):
370 def heads(self):
371 d = self.do_read("heads")
371 d = self.do_read("heads")
372 try:
372 try:
373 return map(bin, d[:-1].split(" "))
373 return map(bin, d[:-1].split(" "))
374 except:
374 except:
375 raise util.UnexpectedOutput(_("unexpected response:"), d)
375 raise util.UnexpectedOutput(_("unexpected response:"), d)
376
376
377 def branches(self, nodes):
377 def branches(self, nodes):
378 n = " ".join(map(hex, nodes))
378 n = " ".join(map(hex, nodes))
379 d = self.do_read("branches", nodes=n)
379 d = self.do_read("branches", nodes=n)
380 try:
380 try:
381 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
381 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
382 return br
382 return br
383 except:
383 except:
384 raise util.UnexpectedOutput(_("unexpected response:"), d)
384 raise util.UnexpectedOutput(_("unexpected response:"), d)
385
385
386 def between(self, pairs):
386 def between(self, pairs):
387 n = "\n".join(["-".join(map(hex, p)) for p in pairs])
387 n = "\n".join(["-".join(map(hex, p)) for p in pairs])
388 d = self.do_read("between", pairs=n)
388 d = self.do_read("between", pairs=n)
389 try:
389 try:
390 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
390 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
391 return p
391 return p
392 except:
392 except:
393 raise util.UnexpectedOutput(_("unexpected response:"), d)
393 raise util.UnexpectedOutput(_("unexpected response:"), d)
394
394
395 def changegroup(self, nodes, kind):
395 def changegroup(self, nodes, kind):
396 n = " ".join(map(hex, nodes))
396 n = " ".join(map(hex, nodes))
397 f = self.do_cmd("changegroup", roots=n)
397 f = self.do_cmd("changegroup", roots=n)
398 return util.chunkbuffer(zgenerator(f))
398 return util.chunkbuffer(zgenerator(f))
399
399
400 def changegroupsubset(self, bases, heads, source):
400 def changegroupsubset(self, bases, heads, source):
401 self.requirecap('changegroupsubset', _('look up remote changes'))
401 self.requirecap('changegroupsubset', _('look up remote changes'))
402 baselst = " ".join([hex(n) for n in bases])
402 baselst = " ".join([hex(n) for n in bases])
403 headlst = " ".join([hex(n) for n in heads])
403 headlst = " ".join([hex(n) for n in heads])
404 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
404 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
405 return util.chunkbuffer(zgenerator(f))
405 return util.chunkbuffer(zgenerator(f))
406
406
407 def unbundle(self, cg, heads, source):
407 def unbundle(self, cg, heads, source):
408 # have to stream bundle to a temp file because we do not have
408 # have to stream bundle to a temp file because we do not have
409 # http 1.1 chunked transfer.
409 # http 1.1 chunked transfer.
410
410
411 type = ""
411 type = ""
412 types = self.capable('unbundle')
412 types = self.capable('unbundle')
413 # servers older than d1b16a746db6 will send 'unbundle' as a
413 # servers older than d1b16a746db6 will send 'unbundle' as a
414 # boolean capability
414 # boolean capability
415 try:
415 try:
416 types = types.split(',')
416 types = types.split(',')
417 except AttributeError:
417 except AttributeError:
418 types = [""]
418 types = [""]
419 if types:
419 if types:
420 for x in types:
420 for x in types:
421 if x in changegroup.bundletypes:
421 if x in changegroup.bundletypes:
422 type = x
422 type = x
423 break
423 break
424
424
425 tempname = changegroup.writebundle(cg, None, type)
425 tempname = changegroup.writebundle(cg, None, type)
426 fp = httpsendfile(tempname, "rb")
426 fp = httpsendfile(tempname, "rb")
427 try:
427 try:
428 try:
428 try:
429 rfp = self.do_cmd(
429 rfp = self.do_cmd(
430 'unbundle', data=fp,
430 'unbundle', data=fp,
431 headers={'content-type': 'application/octet-stream'},
431 headers={'Content-Type': 'application/octet-stream'},
432 heads=' '.join(map(hex, heads)))
432 heads=' '.join(map(hex, heads)))
433 try:
433 try:
434 ret = int(rfp.readline())
434 ret = int(rfp.readline())
435 self.ui.write(rfp.read())
435 self.ui.write(rfp.read())
436 return ret
436 return ret
437 finally:
437 finally:
438 rfp.close()
438 rfp.close()
439 except socket.error, err:
439 except socket.error, err:
440 if err[0] in (errno.ECONNRESET, errno.EPIPE):
440 if err[0] in (errno.ECONNRESET, errno.EPIPE):
441 raise util.Abort(_('push failed: %s') % err[1])
441 raise util.Abort(_('push failed: %s') % err[1])
442 raise util.Abort(err[1])
442 raise util.Abort(err[1])
443 finally:
443 finally:
444 fp.close()
444 fp.close()
445 os.unlink(tempname)
445 os.unlink(tempname)
446
446
447 def stream_out(self):
447 def stream_out(self):
448 return self.do_cmd('stream_out')
448 return self.do_cmd('stream_out')
449
449
450 class httpsrepository(httprepository):
450 class httpsrepository(httprepository):
451 def __init__(self, ui, path):
451 def __init__(self, ui, path):
452 if not has_https:
452 if not has_https:
453 raise util.Abort(_('Python support for SSL and HTTPS '
453 raise util.Abort(_('Python support for SSL and HTTPS '
454 'is not installed'))
454 'is not installed'))
455 httprepository.__init__(self, ui, path)
455 httprepository.__init__(self, ui, path)
456
456
457 def instance(ui, path, create):
457 def instance(ui, path, create):
458 if create:
458 if create:
459 raise util.Abort(_('cannot create new http repository'))
459 raise util.Abort(_('cannot create new http repository'))
460 if path.startswith('https:'):
460 if path.startswith('https:'):
461 return httpsrepository(ui, path)
461 return httpsrepository(ui, path)
462 return httprepository(ui, path)
462 return httprepository(ui, path)
@@ -1,72 +1,72
1 changeset: 0:4cbec7e6f8c4
1 changeset: 0:4cbec7e6f8c4
2 tag: tip
2 tag: tip
3 user: Testing
3 user: Testing
4 date: Thu Jan 01 00:00:00 1970 +0000
4 date: Thu Jan 01 00:00:00 1970 +0000
5 summary: test
5 summary: test
6
6
7 ---- HEADERS
7 ---- HEADERS
8 200 Script output follows
8 200 Script output follows
9 ---- DATA
9 ---- DATA
10 [('Content-type', 'application/atom+xml; charset=ascii')]
10 [('Content-Type', 'application/atom+xml; charset=ascii')]
11 <?xml version="1.0" encoding="ascii"?>
11 <?xml version="1.0" encoding="ascii"?>
12 <feed xmlns="http://www.w3.org/2005/Atom">
12 <feed xmlns="http://www.w3.org/2005/Atom">
13 <!-- Changelog -->
13 <!-- Changelog -->
14 <id>http://127.0.0.1/</id>
14 <id>http://127.0.0.1/</id>
15 <link rel="self" href="http://127.0.0.1/atom-log"/>
15 <link rel="self" href="http://127.0.0.1/atom-log"/>
16 <link rel="alternate" href="http://127.0.0.1/"/>
16 <link rel="alternate" href="http://127.0.0.1/"/>
17 <title>repo Changelog</title>
17 <title>repo Changelog</title>
18 <updated>1970-01-01T00:00:00+00:00</updated>
18 <updated>1970-01-01T00:00:00+00:00</updated>
19
19
20 <entry>
20 <entry>
21 <title>test</title>
21 <title>test</title>
22 <id>http://www.selenic.com/mercurial/#changeset-4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e</id>
22 <id>http://www.selenic.com/mercurial/#changeset-4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e</id>
23 <link href="http://127.0.0.1/rev/4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e"/>
23 <link href="http://127.0.0.1/rev/4cbec7e6f8c42eb52b6b52670e1f7560ae9a101e"/>
24 <author>
24 <author>
25 <name>Testing</name>
25 <name>Testing</name>
26 <email>&#84;&#101;&#115;&#116;&#105;&#110;&#103;</email>
26 <email>&#84;&#101;&#115;&#116;&#105;&#110;&#103;</email>
27 </author>
27 </author>
28 <updated>1970-01-01T00:00:00+00:00</updated>
28 <updated>1970-01-01T00:00:00+00:00</updated>
29 <published>1970-01-01T00:00:00+00:00</published>
29 <published>1970-01-01T00:00:00+00:00</published>
30 <content type="xhtml">
30 <content type="xhtml">
31 <div xmlns="http://www.w3.org/1999/xhtml">
31 <div xmlns="http://www.w3.org/1999/xhtml">
32 <pre xml:space="preserve">test</pre>
32 <pre xml:space="preserve">test</pre>
33 </div>
33 </div>
34 </content>
34 </content>
35 </entry>
35 </entry>
36
36
37 </feed>
37 </feed>
38
38
39 ---- ERRORS
39 ---- ERRORS
40
40
41 ---- HEADERS
41 ---- HEADERS
42 200 Script output follows
42 200 Script output follows
43 ---- DATA
43 ---- DATA
44 [('Content-type', 'text/plain; charset=ascii')]
44 [('Content-Type', 'text/plain; charset=ascii')]
45
45
46 -rw-r--r-- 4 bar
46 -rw-r--r-- 4 bar
47
47
48
48
49
49
50 ---- ERRORS
50 ---- ERRORS
51
51
52 ---- HEADERS
52 ---- HEADERS
53 200 Script output follows
53 200 Script output follows
54 ---- DATA
54 ---- DATA
55 [('Content-type', 'text/plain; charset=ascii')]
55 [('Content-Type', 'text/plain; charset=ascii')]
56
56
57 /repo/
57 /repo/
58
58
59
59
60 ---- ERRORS
60 ---- ERRORS
61
61
62 ---- HEADERS
62 ---- HEADERS
63 200 Script output follows
63 200 Script output follows
64 ---- DATA
64 ---- DATA
65 [('Content-type', 'text/plain; charset=ascii')]
65 [('Content-Type', 'text/plain; charset=ascii')]
66
66
67 -rw-r--r-- 4 bar
67 -rw-r--r-- 4 bar
68
68
69
69
70
70
71 ---- ERRORS
71 ---- ERRORS
72
72
@@ -1,12 +1,12
1 changeset: 0:61c9426e69fe
1 changeset: 0:61c9426e69fe
2 tag: tip
2 tag: tip
3 user: test
3 user: test
4 date: Thu Jan 01 00:00:00 1970 +0000
4 date: Thu Jan 01 00:00:00 1970 +0000
5 summary: test
5 summary: test
6
6
7 ---- HEADERS
7 ---- HEADERS
8 200 Script output follows
8 200 Script output follows
9 ---- DATA
9 ---- DATA
10 [('Content-type', 'text/html; charset=ascii')]
10 [('Content-Type', 'text/html; charset=ascii')]
11 ---- ERRORS
11 ---- ERRORS
12
12
General Comments 0
You need to be logged in to leave comments. Login now